/*-------------------------------------------------------------------------
 *
 * allpaths.c
 *	  查找处理查询的可能搜索路径的例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/backend/optimizer/path/allpaths.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include <limits.h>
#include <math.h>

#include "access/sysattr.h"
#include "access/tsmapi.h"
#include "catalog/pg_class.h"
#include "catalog/pg_operator.h"
#include "catalog/pg_proc.h"
#include "foreign/fdwapi.h"
#include "miscadmin.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/supportnodes.h"
#ifdef OPTIMIZER_DEBUG
#include "nodes/print.h"
#endif
#include "optimizer/appendinfo.h"
#include "optimizer/clauses.h"
#include "optimizer/cost.h"
#include "optimizer/geqo.h"
#include "optimizer/inherit.h"
#include "optimizer/optimizer.h"
#include "optimizer/pathnode.h"
#include "optimizer/paths.h"
#include "optimizer/plancat.h"
#include "optimizer/planner.h"
#include "optimizer/restrictinfo.h"
#include "optimizer/tlist.h"
#include "parser/parse_clause.h"
#include "parser/parsetree.h"
#include "partitioning/partbounds.h"
#include "partitioning/partprune.h"
#include "rewrite/rewriteManip.h"
#include "utils/lsyscache.h"


/* pushdown_safety_info.unsafeFlags的位掩码标志 */
#define UNSAFE_HAS_VOLATILE_FUNC		(1 << 0)
#define UNSAFE_HAS_SET_FUNC				(1 << 1)
#define UNSAFE_NOTIN_DISTINCTON_CLAUSE	(1 << 2)
#define UNSAFE_NOTIN_PARTITIONBY_CLAUSE	(1 << 3)
#define UNSAFE_TYPE_MISMATCH			(1 << 4)

/* subquery_is_pushdown_safe的结果 */
typedef struct pushdown_safety_info
{
	unsigned char *unsafeFlags; /* 原因的位掩码，说明此目标列表
								 * 列对于qual推送不安全，或者为0如果
								 * 没有原因。 */
	bool		unsafeVolatile; /* 不要推送易变的qual */
	bool		unsafeLeaky;	/* 不要推送泄露的qual */
} pushdown_safety_info;

/* qual_is_pushdown_safe的返回类型 */
typedef enum pushdown_safe_type
{
	PUSHDOWN_UNSAFE,			/* 推送qual到子查询中不安全 */
	PUSHDOWN_SAFE,				/* 推送qual到子查询中安全 */
	PUSHDOWN_WINDOWCLAUSE_RUNCOND	/* 不安全，但可能作为WindowClause
									 * 运行条件 */
} pushdown_safe_type;

/* 这些参数由GUC设置 */
bool		enable_geqo = false;	/* 以防GUC没有设置它 */
int			geqo_threshold;
int			min_parallel_table_scan_size;
int			min_parallel_index_scan_size;

/* 插件在set_rel_pathlist()中获取控制的钩子 */
set_rel_pathlist_hook_type set_rel_pathlist_hook = NULL;

/* 插件替换standard_join_search()的钩子 */
join_search_hook_type join_search_hook = NULL;


static void set_base_rel_consider_startup(PlannerInfo *fc_root);
static void set_base_rel_sizes(PlannerInfo *fc_root);
static void set_base_rel_pathlists(PlannerInfo *fc_root);
static void set_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						 Index fc_rti, RangeTblEntry *fc_rte);
static void set_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							 Index fc_rti, RangeTblEntry *fc_rte);
static void set_plain_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							   RangeTblEntry *fc_rte);
static void fc_create_plain_partial_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel);
static void set_rel_consider_parallel(PlannerInfo *fc_root, RelOptInfo *fc_rel,
									  RangeTblEntry *fc_rte);
static void set_plain_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								   RangeTblEntry *fc_rte);
static void set_tablesample_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
									 RangeTblEntry *fc_rte);
static void set_tablesample_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
										 RangeTblEntry *fc_rte);
static void set_foreign_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							 RangeTblEntry *fc_rte);
static void set_foreign_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								 RangeTblEntry *fc_rte);
static void set_append_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								Index fc_rti, RangeTblEntry *fc_rte);
static void set_append_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
									Index fc_rti, RangeTblEntry *fc_rte);
static void fc_generate_orderedappend_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel,
										 List *fc_live_childrels,
										 List *fc_all_child_pathkeys);
static Path *get_cheapest_parameterized_child_path(PlannerInfo *fc_root,
												   RelOptInfo *fc_rel,
												   Relids fc_required_outer);
static void fc_accumulate_append_subpath(Path *fc_path,
									  List **fc_subpaths,
									  List **fc_special_subpaths);
static Path *get_singleton_append_subpath(Path *fc_path);
static void set_dummy_rel_pathlist(RelOptInfo *fc_rel);
static void set_subquery_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								  Index fc_rti, RangeTblEntry *fc_rte);
static void set_function_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								  RangeTblEntry *fc_rte);
static void set_values_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								RangeTblEntry *fc_rte);
static void set_tablefunc_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								   RangeTblEntry *fc_rte);
static void set_cte_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							 RangeTblEntry *fc_rte);
static void set_namedtuplestore_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
										 RangeTblEntry *fc_rte);
static void set_result_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								RangeTblEntry *fc_rte);
static void set_worktable_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								   RangeTblEntry *fc_rte);
static RelOptInfo *fc_make_rel_from_joinlist(PlannerInfo *fc_root, List *fc_joinlist);
static bool fc_subquery_is_pushdown_safe(Query *fc_subquery, Query *fc_topquery,
									  pushdown_safety_info *fc_safetyInfo);
static bool fc_recurse_pushdown_safe(Node *fc_setOp, Query *fc_topquery,
								  pushdown_safety_info *fc_safetyInfo);
static void fc_check_output_expressions(Query *fc_subquery,
									 pushdown_safety_info *fc_safetyInfo);
static void fc_compare_tlist_datatypes(List *fc_tlist, List *fc_colTypes,
									pushdown_safety_info *fc_safetyInfo);
static bool fc_targetIsInAllPartitionLists(TargetEntry *fc_tle, Query *fc_query);
static pushdown_safe_type fc_qual_is_pushdown_safe(Query *fc_subquery, Index fc_rti,
												RestrictInfo *fc_rinfo,
												pushdown_safety_info *fc_safetyInfo);
static void fc_subquery_push_qual(Query *fc_subquery,
							   RangeTblEntry *fc_rte, Index fc_rti, Node *fc_qual);
static void fc_recurse_push_qual(Node *fc_setOp, Query *fc_topquery,
							  RangeTblEntry *fc_rte, Index fc_rti, Node *fc_qual);
static void fc_remove_unused_subquery_outputs(Query *fc_subquery, RelOptInfo *fc_rel,
										   Bitmapset *fc_extra_used_attrs);


/*
 * make_one_rel
 *	  找到执行查询的所有可能访问路径，返回一个
 *	  表示查询中所有基础关系的连接的单一关系。
 */
RelOptInfo * make_one_rel(PlannerInfo *fc_root, List *fc_joinlist)
{
	RelOptInfo *fc_rel;
	Index		fc_rti;
	double		fc_total_pages;

	/*
	 * 构造all_baserels Relids集。
	 */
	fc_root->all_baserels = NULL;
	for (fc_rti = 1; fc_rti < fc_root->simple_rel_array_size; fc_rti++)
	{
		RelOptInfo *fc_brel = fc_root->simple_rel_array[fc_rti];

		/* 可能会有空位对应于非基础关系的RTE */
		if (fc_brel == NULL)
			continue;

		Assert(fc_brel->relid == fc_rti); /* 对数组进行完整性检查 */

		/* 忽略被视为“其他关系”的RTE */
		if (fc_brel->reloptkind != RELOPT_BASEREL)
			continue;

		fc_root->all_baserels = bms_add_member(fc_root->all_baserels, fc_brel->relid);
	}

	/* 标记基础关系是否关心快速启动计划 */
	set_base_rel_consider_startup(fc_root);

	/*
	 * 计算每个基础关系的大小估算和consider_parallel标志。
	 */
	set_base_rel_sizes(fc_root);

	/*
	 * 现在我们应该已经拥有查询中涉及的每个实际表的大小估算，
	 * 我们也知道哪些如果有则已经被通过连接移除、通过
	 * 分区剪枝进行修剪，或通过约束排除而消除。
	 * 现在我们可以计算total_table_pages。
	 *
	 * 注意，appendrels在这里没有被重复计算，即使我们没有
	 * 办法区分appendrels的RelOptInfos，因为父关系的页面为0。
	 *
	 * XXX 如果一个表是自连接的，我们将按出现次数计算一次，
	 * 这也许是错误的......但这并不完全清楚，
	 * 这里检测自连接是困难的，所以先忽略它。
	 */
	fc_total_pages = 0;
	for (fc_rti = 1; fc_rti < fc_root->simple_rel_array_size; fc_rti++)
	{
		RelOptInfo *fc_brel = fc_root->simple_rel_array[fc_rti];

		if (fc_brel == NULL)
			continue;

		Assert(fc_brel->relid == fc_rti); /* 对数组进行完整性检查 */

		if (IS_DUMMY_REL(fc_brel))
			continue;

		if (IS_SIMPLE_REL(fc_brel))
			fc_total_pages += (double) fc_brel->pages;
	}
	fc_root->total_table_pages = fc_total_pages;

	/*
	 * 为每个基础关系生成访问路径。
	 */
	set_base_rel_pathlists(fc_root);

	/*
	 * 生成整个连接树的访问路径。
	 */
	fc_rel = fc_make_rel_from_joinlist(fc_root, fc_joinlist);

	/*
	 * 结果应该连接所有且仅查询的基础关系。
	 */
	Assert(bms_equal(fc_rel->relids, fc_root->all_baserels));

	return fc_rel;
}

/*
 * set_base_rel_consider_startup
 *	  设置每个基础关系条目的 consider_[param_]startup 标志。
 *
 * 目前我们这里只处理 consider_param_startup；因为
 * consider_startup 的逻辑非常简单，并且对每个基础
 * 关系都是相同的，我们只让 build_simple_rel() 正确初始化该标志
 * 以便开始。如果该逻辑变得更复杂，可能将其移动到这里更好。
 */
static void set_base_rel_consider_startup(PlannerInfo *fc_root)
{
	/*
	 * 由于参数化路径只能在嵌套循环连接计划的内部使用，
	 * 因此考虑快速启动计划的价值通常很小。然而，对于在
	 * SEMI 或 ANTI 连接的右侧关系，快速启动计划可能是有用的，
	 * 因为我们只关心获取一个元组。
	 *
	 * 为了最小化规划时间的增长，我们目前将此限制为
	 * 右侧是单个基础关系而不是连接的情况；并且没有
	 * provisions 供 consider_param_startup 在 joinrels 上被设置。
	 * 同样，我们也不担心 appendrels。costsize.c 对嵌套循环半
	 * 反连接的成本规则也不考虑这些情况。
	 */
	ListCell   *fc_lc;

	foreach(fc_lc, fc_root->join_info_list)
	{
		SpecialJoinInfo *fc_sjinfo = (SpecialJoinInfo *) lfirst(fc_lc);
		int			fc_varno;

		if ((fc_sjinfo->jointype == JOIN_SEMI || fc_sjinfo->jointype == JOIN_ANTI) &&
			bms_get_singleton_member(fc_sjinfo->syn_righthand, &fc_varno))
		{
			RelOptInfo *fc_rel = find_base_rel(fc_root, fc_varno);

			fc_rel->consider_param_startup = true;
		}
	}
}

/*
 * set_base_rel_sizes
 *	  设置每个基础关系条目的大小估计（行数和宽度）。
 *	  还确定是否考虑基础关系的并行路径。
 *
 * 我们在对基础关系的单独遍历中执行此操作，以便
 * 行数估计可用于参数化路径生成，并且在我们开始
 * 生成路径之前，每个关系的 consider_parallel 标志
 * 会被正确设置。
 */
static void set_base_rel_sizes(PlannerInfo *fc_root)
{
	Index		fc_rti;

	for (fc_rti = 1; fc_rti < fc_root->simple_rel_array_size; fc_rti++)
	{
		RelOptInfo *fc_rel = fc_root->simple_rel_array[fc_rti];
		RangeTblEntry *fc_rte;

		/* 可能会有空位对应于非基础关系的RTE */
		if (fc_rel == NULL)
			continue;

		Assert(fc_rel->relid == fc_rti);	/* 对数组进行完整性检查 */

		/* 忽略被视为“其他关系”的RTE */
		if (fc_rel->reloptkind != RELOPT_BASEREL)
			continue;

		fc_rte = fc_root->simple_rte_array[fc_rti];

		/*
		 * 如果通常允许对该查询进行并行处理，检查
		 * 是否特别允许对该关系进行并行处理。我们必须在
		 * set_rel_size() 之前执行此操作，因为（a）如果该
		 * 关系是继承父关系，set_append_rel_size() 将使用
		 * 并可能更改关系的 consider_parallel 标志，以及
		 * （b）对于某些 RTE 类型，set_rel_size()
		 * 会立即开始生成路径。
		 */
		if (fc_root->glob->parallelModeOK)
			set_rel_consider_parallel(fc_root, fc_rel, fc_rte);

		set_rel_size(fc_root, fc_rel, fc_rti, fc_rte);
	}
}

/*
 * set_base_rel_pathlists
 *	  查找可供扫描每个基础关系条目的所有路径。
 *	  考虑顺序扫描和任何可用的索引。
 *	  每个有用的路径附加到其关系的 'pathlist' 字段。
 */
static void set_base_rel_pathlists(PlannerInfo *fc_root)
{
	Index		fc_rti;

	for (fc_rti = 1; fc_rti < fc_root->simple_rel_array_size; fc_rti++)
	{
		RelOptInfo *fc_rel = fc_root->simple_rel_array[fc_rti];

		/* 可能会有空位对应于非基础关系的RTE */
		if (fc_rel == NULL)
			continue;

		Assert(fc_rel->relid == fc_rti);	/* 对数组进行完整性检查 */

		/* 忽略被视为“其他关系”的RTE */
		if (fc_rel->reloptkind != RELOPT_BASEREL)
			continue;

		set_rel_pathlist(fc_root, fc_rel, fc_rti, fc_root->simple_rte_array[fc_rti]);
	}
}

/*
 * set_rel_size
 *	  设置基础关系的大小估计
 */
static void set_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
			 Index fc_rti, RangeTblEntry *fc_rte)
{
	if (fc_rel->reloptkind == RELOPT_BASEREL &&
		relation_excluded_by_constraints(fc_root, fc_rel, fc_rte))
	{
		/*
		 * 我们证明不需要通过约束排除来扫描关系，
		 * 因此为其设置一个单一的虚拟路径。在此我们仅仅
		 * 对常规基础关系进行检查；如果是其他关系，CE
		 * 已在 set_append_rel_size() 中进行了检查。
		 *
		 * 在这种情况下，我们立即设置关系的路径，而
		 * 不是让 set_rel_pathlist 来处理。这是因为
		 * 我们没有约定来标记关系为虚拟关系，除了
		 * 分配一个虚拟路径给它。
		 */
		set_dummy_rel_pathlist(fc_rel);
	}
	else if (fc_rte->inh)
	{
		/* 这是一个“附加关系”，相应处理 */
		set_append_rel_size(fc_root, fc_rel, fc_rti, fc_rte);
	}
	else
	{
		switch (fc_rel->rtekind)
		{
			case RTE_RELATION:
				if (fc_rte->relkind == RELKIND_FOREIGN_TABLE)
				{
					/* 外部表 */
					set_foreign_size(fc_root, fc_rel, fc_rte);
				}
				else if (fc_rte->relkind == RELKIND_PARTITIONED_TABLE)
				{
					/*
					 * 如果被要求扫描仅仅的分区表，
					 * 我们可能会到达这里。在这种情况下，
					 * 我们不应该扫描任何分区，因此将其标记为虚拟关系。
					 */
					set_dummy_rel_pathlist(fc_rel);
				}
				else if (fc_rte->tablesample != NULL)
				{
					/* 采样关系 */
					set_tablesample_rel_size(fc_root, fc_rel, fc_rte);
				}
				else
				{
					/* 普通关系 */
					set_plain_rel_size(fc_root, fc_rel, fc_rte);
				}
				break;
			case RTE_SUBQUERY:

				/*
				 * 子查询不支持在参数化路径和未参数化路径之间进行选择，
				 * 所以直接构建它们的路径。
				 */
				set_subquery_pathlist(fc_root, fc_rel, fc_rti, fc_rte);
				break;
			case RTE_FUNCTION:
				set_function_size_estimates(fc_root, fc_rel);
				break;
			case RTE_TABLEFUNC:
				set_tablefunc_size_estimates(fc_root, fc_rel);
				break;
			case RTE_VALUES:
				set_values_size_estimates(fc_root, fc_rel);
				break;
			case RTE_CTE:

				/*
				 * CTE 不支持在参数化路径和未参数化路径之间进行选择，
				 * 所以直接构建它们的路径。
				 */
				if (fc_rte->self_reference)
					set_worktable_pathlist(fc_root, fc_rel, fc_rte);
				else
					set_cte_pathlist(fc_root, fc_rel, fc_rte);
				break;
			case RTE_NAMEDTUPLESTORE:
				/* 不如立即构建路径 */
				set_namedtuplestore_pathlist(fc_root, fc_rel, fc_rte);
				break;
			case RTE_RESULT:
				/* 不如立即构建路径 */
				set_result_pathlist(fc_root, fc_rel, fc_rte);
				break;
			default:
				elog(ERROR, "unexpected rtekind: %d", (int) fc_rel->rtekind);
				break;
		}
	}

	/*
	 * 我们坚持要求所有非虚拟关系具有非零行数估计。
	 */
	Assert(fc_rel->rows > 0 || IS_DUMMY_REL(fc_rel));
}

/*
 * set_rel_pathlist
 *	  为基础关系构建访问路径
 */
static void set_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
				 Index fc_rti, RangeTblEntry *fc_rte)
{
	if (IS_DUMMY_REL(fc_rel))
	{
		/* 我们已经证明关系为空，因此没有更多要做的 */
	}
	else if (fc_rte->inh)
	{
		/* 这是一个“附加关系”，相应处理 */
		set_append_rel_pathlist(fc_root, fc_rel, fc_rti, fc_rte);
	}
	else
	{
		switch (fc_rel->rtekind)
		{
			case RTE_RELATION:
				if (fc_rte->relkind == RELKIND_FOREIGN_TABLE)
				{
					/* 外部表 */
					set_foreign_pathlist(fc_root, fc_rel, fc_rte);
				}
				else if (fc_rte->tablesample != NULL)
				{
					/* 采样关系 */
					set_tablesample_rel_pathlist(fc_root, fc_rel, fc_rte);
				}
				else
				{
					/* 普通关系 */
					set_plain_rel_pathlist(fc_root, fc_rel, fc_rte);
				}
				break;
			case RTE_SUBQUERY:
				/* 子查询 --- 在 set_rel_size 中完全处理 */
				break;
			case RTE_FUNCTION:
				/* 范围函数 */
				set_function_pathlist(fc_root, fc_rel, fc_rte);
				break;
			case RTE_TABLEFUNC:
				/* 表函数 */
				set_tablefunc_pathlist(fc_root, fc_rel, fc_rte);
				break;
			case RTE_VALUES:
				/* 值列表 */
				set_values_pathlist(fc_root, fc_rel, fc_rte);
				break;
			case RTE_CTE:
				/* CTE 引用 --- 在 set_rel_size 中完全处理 */
				break;
			case RTE_NAMEDTUPLESTORE:
				/* tuplestore 引用 --- 在 set_rel_size 中完全处理 */
				break;
			case RTE_RESULT:
				/* 简单结果 --- 在 set_rel_size 中完全处理 */
				break;
			default:
				elog(ERROR, "unexpected rtekind: %d", (int) fc_rel->rtekind);
				break;
		}
	}

	/*
	 * 允许插件对该基础关系的路径集进行编辑。它可以通过调用
	 * add_path() 添加新的路径（例如 CustomPaths），或在
	 * 关注并行时调用 add_partial_path()。它也可以删除或修改核心代码添加的路径。
	 */
	if (set_rel_pathlist_hook)
		(*set_rel_pathlist_hook) (fc_root, fc_rel, fc_rti, fc_rte);

	
	if (fc_rel->reloptkind == RELOPT_BASEREL &&
		!bms_equal(fc_rel->relids, fc_root->all_baserels))
		generate_useful_gather_paths(fc_root, fc_rel, false);

	/* 现在找到这个关系的最便宜路径 */
	set_cheapest(fc_rel);

#ifdef OPTIMIZER_DEBUG
	debug_print_rel(fc_root, fc_rel);
#endif
}

/* 
 * set_plain_rel_size
 *	  为一个普通关系（无子查询，无继承）设置大小估计
 */
static void set_plain_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	/* 
	 * 测试任何部分索引在关系上的适用性。我们必须首先做这个
	 * 因为部分唯一索引可能会影响大小估计。
	 */
	check_index_predicates(fc_root, fc_rel);

	/* 标记关系的估计输出行数、宽度等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/* 
 * 如果这个关系可能会在工作线程中扫描，则设置
 * 其 consider_parallel 标志。
 */
static void set_rel_consider_parallel(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						  RangeTblEntry *fc_rte)
{
	/* 
	 * 该标志之前已初始化为 false，因此如果明显无法安全地设置
	 * 它，我们可以直接返回。
	 */
	Assert(!fc_rel->consider_parallel);

	/* 如果整个查询不允许并行，则不要调用此函数。 */
	Assert(fc_root->glob->parallelModeOK);

	/* 此函数仅应针对 baserels 和 appendrel 子项调用。 */
	Assert(IS_SIMPLE_REL(fc_rel));

	/* 基于 rtekind 的各种检查。 */
	switch (fc_rte->rtekind)
	{
		case RTE_RELATION:

			/* 
			 * 当前，平行工作者无法访问领导者的临时
			 * 表。我们可能可以放宽这一点，如果我们在查询开始时
			 * 写入其所有本地缓冲区，并且随后不做更改（也许可以允许提示位更改），
			 * 如果我们教工作线程读取它们。虽然写入大量的
			 * 临时缓冲区可能会很昂贵，而且我们现在没有
			 * 所需的其他基础设施。因此目前，如果我们看到临时表
			 * ，就退出。
			 */
			if (get_rel_persistence(fc_rte->relid) == RELPERSISTENCE_TEMP)
				return;

			/* 
			 * 如果样本函数及其参数是安全的，表采样可以下放到工作者。 */
			if (fc_rte->tablesample != NULL)
			{
				char		fc_proparallel = func_parallel(fc_rte->tablesample->tsmhandler);

				if (fc_proparallel != PROPARALLEL_SAFE)
					return;
				if (!is_parallel_safe(fc_root, (Node *) fc_rte->tablesample->args))
					return;
			}

			/* 
			 * 询问 FDWs 它们是否可以支持在工作线程中执行 ForeignScan。
			 * 通常，答案是否定的。例如，如果 FDW 的性质使它与远程服务器
			 * 开启 TCP 连接，每个并行工作者将拥有一个独立的连接，而这些连接可能无法
			 * 在工作者和领导者之间适当地协调。
			 */
			if (fc_rte->relkind == RELKIND_FOREIGN_TABLE)
			{
				Assert(fc_rel->fdwroutine);
				if (!fc_rel->fdwroutine->IsForeignScanParallelSafe)
					return;
				if (!fc_rel->fdwroutine->IsForeignScanParallelSafe(fc_root, fc_rel, fc_rte))
					return;
			}

			/* 
			 * 对于 appendrels，存在额外的考虑，我们将在 set_append_rel_size 和
			 * set_append_rel_pathlist 中处理。现在，只需根据关系自身的
			 * quals 和 targetlist 设置 consider_parallel。
			 */
			break;

		case RTE_SUBQUERY:

			/* 
			 * 在平行工作者中扫描 FROM 中的子查询（与 SubPlan 或
			 * InitPlan 不同）没有内在的问题。如果子查询恰巧没有
			 * 任何并行安全路径，那么将其标记为 consider_parallel 不会改变任何
			 * 事情，但这个情况对于普通表也成立。我们必须根据关系自身的 
			 * quals 和 targetlist 设置 consider_parallel，
			 * 以便如果子查询路径是并行安全的，但我们附加的 quals 和
			 * 投影不是，我们正确地将 SubqueryScanPath 标记为不并行安全。
			 * （注意 set_subquery_pathlist() 可能将其中一些 quals 
			 * 下推到子查询本身，但这并不会改变任何事情。）
			 *
			 * 我们不能将包含 LIMIT/OFFSET 的子查询下推到工作者，因为没有
			 * 保证行顺序是完全确定的，应用 LIMIT/OFFSET 会导致
			 * 顶层结果不一致。（在某些情况下，结果是有序的，我们可以放宽这一限制。
			 * 但是目前看来不值得花费额外的精力去做到这一点。）
			 */
			{
				Query	   *fc_subquery = castNode(Query, fc_rte->subquery);

				if (limit_needed(fc_subquery))
					return;
			}
			break;

		case RTE_JOIN:
			/* 不应该发生；我们这里只考虑 baserels。 */
			Assert(false);
			return;

		case RTE_FUNCTION:
			/* 检查并行限制函数。 */
			if (!is_parallel_safe(fc_root, (Node *) fc_rte->functions))
				return;
			break;

		case RTE_TABLEFUNC:
			/* 不并行安全 */
			return;

		case RTE_VALUES:
			/* 检查并行限制函数。 */
			if (!is_parallel_safe(fc_root, (Node *) fc_rte->values_lists))
				return;
			break;

		case RTE_CTE:

			/* 
			 * CTE 元组存储在平行工作者之间不是共享的，因此我们
			 * 强制所有 CTE 扫描都在领导者中进行。此外，填充
			 * CTE 将需要执行一个在工作者中不可用的子计划，可能
			 * 是并行限制的，并且必须仅执行一次。
			 */
			return;

		case RTE_NAMEDTUPLESTORE:

			/* 
			 * 元组存储无法共享，至少在没有更多基础设施支持的情况下。 */
			return;

		case RTE_RESULT:
			/* RESULT RTE 本身没有问题。 */
			break;
	}

	/* 
	 * 如果 baserestrictinfo 中有任何并行限制项，我们
	 * 就放弃对该关系的并行访问。我们可以考虑推迟
	 * 对限制 quals 的应用，直到我们在计划树中的所有并行性
	 * 之上，但并不清楚这种做法在很多情况下会取得成功，
	 * 并且可能很难使外连接子句正确工作。这也可能会打破等价
	 * 类。
	 */
	if (!is_parallel_safe(fc_root, (Node *) fc_rel->baserestrictinfo))
		return;

	/* 
	 * 同样，如果关系的输出不并行安全，则放弃。
	 * （通常，它们只是变量，但有时并非如此。）
	 */
	if (!is_parallel_safe(fc_root, (Node *) fc_rel->reltarget->exprs))
		return;

	/* 我们有了一个赢家。 */
	fc_rel->consider_parallel = true;
}

/* 
 * set_plain_rel_pathlist
 *	  为一个普通关系（无子查询，无继承）构建访问路径
 */
static void set_plain_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;

	/* 
	 * 我们不支持将连接子句下推到 seqscan 的 quals 中，但
	 * 由于其 tlist 中的 LATERAL 引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	
/* 考虑顺序扫描 */
	add_path(fc_rel, create_seqscan_path(fc_root, fc_rel, fc_required_outer, 0));

	/* 如果合适，考虑并行顺序扫描 */
	if (fc_rel->consider_parallel && fc_required_outer == NULL)
		fc_create_plain_partial_paths(fc_root, fc_rel);

	/* 考虑索引扫描 */
	create_index_paths(fc_root, fc_rel);

	/* 考虑TID扫描 */
	create_tidscan_paths(fc_root, fc_rel);
}

/*
 * create_plain_partial_paths
 *	  为平面关系的并行扫描构建部分访问路径
 */
static void fc_create_plain_partial_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	int			fc_parallel_workers;

	fc_parallel_workers = compute_parallel_worker(fc_rel, fc_rel->pages, -1,
											   max_parallel_workers_per_gather);

	/* 如果任何限制设置为零，用户不想要并行扫描。 */
	if (fc_parallel_workers <= 0)
		return;

	/* 基于并行顺序扫描添加一个无序部分路径。 */
	add_partial_path(fc_rel, create_seqscan_path(fc_root, fc_rel, NULL, fc_parallel_workers));
}

/*
 * set_tablesample_rel_size
 *	  设置采样关系的大小估计
 */
static void set_tablesample_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	TableSampleClause *fc_tsc = fc_rte->tablesample;
	TsmRoutine *fc_tsm;
	BlockNumber fc_pages;
	double		fc_tuples;

	/* 
	 * 测试任何部分索引在关系上的适用性。我们必须首先做这个
	 * 因为部分唯一索引可能会影响大小估计。
	 */
	check_index_predicates(fc_root, fc_rel);

	/*
	 * 调用采样方法的估算函数以估计它将读取的页面数量和它将返回的元组数量。(注意：我们假设函数返回合理的值。)
	 */
	fc_tsm = GetTsmRoutine(fc_tsc->tsmhandler);
	fc_tsm->SampleScanGetSampleSize(fc_root, fc_rel, fc_tsc->args,
								 &fc_pages, &fc_tuples);

	/*
	 * 目前，由于我们只会考虑一个SampleScan路径用于关系，因此可以仅覆盖整个关系的页面和元组估计。如果将来我们考虑多个路径类型用于采样关系，我们将需要更复杂的处理。
	 */
	fc_rel->pages = fc_pages;
	fc_rel->tuples = fc_tuples;

	/* 标记关系的估计输出行数、宽度等 */
	set_baserel_size_estimates(fc_root, fc_rel);
}

/*
 * set_tablesample_rel_pathlist
 *	  为采样关系构建访问路径
 */
static void set_tablesample_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;
	Path	   *fc_path;

	/*
	 * 我们不支持将连接子句推入samplescan的qual中，但由于其tlist或TABLESAMPLE参数中的LATERAL引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 考虑采样扫描 */
	fc_path = create_samplescan_path(fc_root, fc_rel, fc_required_outer);

	/*
	 * 如果采样方法不支持可重复扫描，我们必须避免多次扫描该关系的计划。理想情况下，我们只需避免将关系放在嵌套循环连接的内部；但将这种考虑添加到规划器似乎是为了支持不常见的二流采样方法而造成的巨大复杂性。相反，如果存在查询可能进行不安全连接的风险，就将SampleScan包裹在Materialize节点中。我们可以通过计算all_baserels的成员资格来检查连接（注意，这正确地将继承树计算为单个关系）。如果我们在子查询中，我们无法轻易检查外部查询中是否可能发生连接，因此只需假设可能发生一个。
	 *
	 * GetTsmRoutine相对其他测试来说是相对昂贵的，因此即使这有点奇怪，也会最后检查repeatable_across_scans。
	 */
	if ((fc_root->query_level > 1 ||
		 bms_membership(fc_root->all_baserels) != BMS_SINGLETON) &&
		!(GetTsmRoutine(fc_rte->tablesample->tsmhandler)->repeatable_across_scans))
	{
		fc_path = (Path *) create_material_path(fc_rel, fc_path);
	}

	add_path(fc_rel, fc_path);

	/* 目前，至少没有其他路径可考虑 */
}

/*
 * set_foreign_size
 *		为外部表RTE设置大小估计
 */
static void set_foreign_size(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	/* 标记关系的估计输出行数、宽度等 */
	set_foreign_size_estimates(fc_root, fc_rel);

	/* 让FDW调整大小估计，如果可以的话 */
	fc_rel->fdwroutine->GetForeignRelSize(fc_root, fc_rel, fc_rte->relid);

	/* ... 但不要让它将行估计设置为零 */
	fc_rel->rows = clamp_row_est(fc_rel->rows);

	/*
	 * 此外，确保rel->tuples相对于rel->rows不是不合理的。特别是，这确保了如果pg_class.reltuples包含-1而FDW不做任何事情来替换它时是合理的。
	 */
	fc_rel->tuples = Max(fc_rel->tuples, fc_rel->rows);
}

/*
 * set_foreign_pathlist
 *		为外部表RTE构建访问路径
 */
static void set_foreign_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	/* 调用FDW的GetForeignPaths函数以生成路径 */
	fc_rel->fdwroutine->GetForeignPaths(fc_root, fc_rel, fc_rte->relid);
}

/*
 * set_append_rel_size
 *	  为简单的“追加关系”设置大小估计
 *
 * 传入的rel和RTE表示整个追加关系。关系的内容是通过将各个成员关系的输出连接在一起计算得出的。注意在非分区继承情况下，第一个成员关系实际上是与父RTE中提到的相同表……但它有不同的RTE和RelOptInfo。这是一个好事，因为它们的输出大小不同。
 */
static void set_append_rel_size(PlannerInfo *fc_root, RelOptInfo *fc_rel,
					Index fc_rti, RangeTblEntry *fc_rte)
{
	int			fc_parentRTindex = fc_rti;
	bool		fc_has_live_children;
	double		fc_parent_rows;
	double		fc_parent_size;
	double	   *fc_parent_attrsizes;
	int			fc_nattrs;
	ListCell   *fc_l;

	/* 防止由于过深的继承树导致的堆栈溢出。 */
	check_stack_depth();

	Assert(IS_SIMPLE_REL(fc_rel));

	/*
	 * 如果这是一个分区的基关系，设置consider_partitionwise_join标志；当前，我们仅在目标列表不包含整个行Var时才考虑基关系的分区连接。
	 */
	if (enable_partitionwise_join &&
		fc_rel->reloptkind == RELOPT_BASEREL &&
		fc_rte->relkind == RELKIND_PARTITIONED_TABLE &&
		fc_rel->attr_needed[InvalidAttrNumber - fc_rel->min_attr] == NULL)
		fc_rel->consider_partitionwise_join = true;

	/*
	 * 初始化以计算整个追加关系的大小估计。
	 *
	 * 我们通过根据子关系的行数加权不同子关系的宽度来处理宽度估计。这是合理的，因为使用宽度估计主要是为了计算总关系的“占用空间”，如果我们必须对其进行排序或哈希。为此，我们将总的等效大小（以“double”算术计算）相加，然后除以总行数估计。这是为总关系宽度和每个属性单独完成的。
	 *
	 * 注意：如果你考虑更改这个逻辑，请注意子关系可能没有零行和/或宽度，如果它们被约束排除。
	 */
	fc_has_live_children = false;
	fc_parent_rows = 0;
	fc_parent_size = 0;
	fc_nattrs = fc_rel->max_attr - fc_rel->min_attr + 1;
	fc_parent_attrsizes = (double *) palloc0(fc_nattrs * sizeof(double));

	foreach(fc_l, fc_root->append_rel_list)
	{
		AppendRelInfo *fc_appinfo = (AppendRelInfo *) lfirst(fc_l);
		int			fc_childRTindex;
		RangeTblEntry *fc_childRTE;
		RelOptInfo *fc_childrel;
		ListCell   *fc_parentvars;
		ListCell   *fc_childvars;

		/* append_rel_list包含所有追加关系；忽略其他 */
		if (fc_appinfo->parent_relid != fc_parentRTindex)
			continue;

		fc_childRTindex = fc_appinfo->child_relid;
		fc_childRTE = fc_root->simple_rte_array[fc_childRTindex];

		/*
		 * 子关系的RelOptInfo是在add_other_rels_to_query期间创建的。
		 */
		fc_childrel = find_base_rel(fc_root, fc_childRTindex);
		Assert(fc_childrel->reloptkind == RELOPT_OTHER_MEMBER_REL);

		/* 我们可能已经证明子关系是虚拟的。 */
		if (IS_DUMMY_REL(fc_childrel))
			continue;

		/*
		 * 我们必须将父关系的目标列表和qual复制到子关系中，并适当替换变量。然而，baserestrictinfo的qual在构建子关系的RelOptInfo时已经被复制/替换了。因此，我们在应用约束排除之前不需要任何额外的设置。
		 */
		if (relation_excluded_by_constraints(fc_root, fc_childrel, fc_childRTE))
		{
			/*
			 * 这个子关系不需要被扫描，因此我们可以将其从追加关系中省略。
			 */
			set_dummy_rel_pathlist(fc_childrel);
			continue;
		}

		
/*
		 * 约束排除失败，因此将父节点的连接条件和目标列表复制到子节点，并进行适当的变量替换。
		 *
		 * 注意：结果中的 childrel->reltarget->exprs 可能包含任意表达式，而这些表达式通常不会出现在关系的目标列表中。
		 * 可能查看附加关系子节点的代码必须应对这种情况。(通常，关系的目标列表只包括 Vars 和 PlaceHolderVars。) XXX 我们不打算
		 * 更新 childrel->reltarget 的成本或宽度字段；目前尚不清楚这是否有用。
		 */
		fc_childrel->joininfo = (List *)
			adjust_appendrel_attrs(fc_root,
								   (Node *) fc_rel->joininfo,
								   1, &fc_appinfo);
		fc_childrel->reltarget->exprs = (List *)
			adjust_appendrel_attrs(fc_root,
								   (Node *) fc_rel->reltarget->exprs,
								   1, &fc_appinfo);

		/*
		 * 我们还必须在等价类数据结构中创建子项。这是必要的，无论是父节点参与某些等价类连接（因为我们希望考虑
		 * 针对各个子节点的内部索引扫描连接），还是父节点具有有用的路径键（因为我们应该尝试构建产生这些排序
		 * 的 MergeAppend 路径）。
		 */
		if (fc_rel->has_eclass_joins || has_useful_pathkeys(fc_root, fc_rel))
			add_child_rel_equivalences(fc_root, fc_appinfo, fc_rel, fc_childrel);
		fc_childrel->has_eclass_joins = fc_rel->has_eclass_joins;

		/*
		 * 注意：我们可以通过通过翻译的变量映射转换父节点的 attr_needed 数据，为子节点的变量计算适当的
		 * attr_needed 数据。然而，目前没有必要，因为 attr_needed 仅针对基关系进行检查，而不对其他关系进行检查。
		 * 所以我们只是把子节点的 attr_needed 留空。
		 */

		/*
		 * 如果我们考虑与父关系的分区连接，那么对子关系也进行同样的操作。
		 *
		 * 注意：这里我们滥用 consider_partitionwise_join 标志，通过为那些本身并未分区的子关系设置它。我们这样做是为了
		 * 告诉 try_partitionwise_join()，即便它随后被证明是虚拟的，子关系也是足够有效的，可以作为每个分区的输入。（在
		 * 我们设置了 reltarget 和 EC 条目之前，它是不可用的，正如我们刚刚做的那样。）
		 */
		if (fc_rel->consider_partitionwise_join)
			fc_childrel->consider_partitionwise_join = true;

		/*
		 * 如果总体上允许对该查询进行并行处理，请查看是否特别允许对该子关系进行并行处理。但是，如果我们已经决定
		 * 附加关系整体上不安全进行并行处理，那么考虑在该子节点上进行并行处理是没有意义的。为了保持一致性，在
		 * 为子节点调用 set_rel_size() 之前执行此操作。
		 */
		if (fc_root->glob->parallelModeOK && fc_rel->consider_parallel)
			set_rel_consider_parallel(fc_root, fc_childrel, fc_childRTE);

		/*
		 * 计算子查询的大小。
		 */
		set_rel_size(fc_root, fc_childrel, fc_childRTindex, fc_childRTE);

		/*
		 * 有可能约束排除在子查询中检测到矛盾，即使我们在上面没有证明之一。如果
		 * 是这样，我们可以跳过这个子查询。
		 */
		if (IS_DUMMY_REL(fc_childrel))
			continue;

		/* 我们至少有一个活动的子查询。 */
		fc_has_live_children = true;

		/*
		 * 如果任何活动子查询不是并行安全的，则将整个 appendrel 视为不并行安全。
		 * 将来我们可能能够生成计划，其中一些子查询被分配给工作者，而其他的则不；
		 * 但我们今天没有做到这一点，因此在 appendrel 中考虑任何部分路径是浪费，
		 * 除非它全部是安全的。 
		 * （在访问到这个子查询之前访问的子查询将不会在
		 * set_append_rel_pathlist() 中被标记。）
		 */
		if (!fc_childrel->consider_parallel)
			fc_rel->consider_parallel = false;

		/*
		 * 从每个活动子查询中累积大小信息。
		 */
		Assert(fc_childrel->rows > 0);

		fc_parent_rows += fc_childrel->rows;
		fc_parent_size += fc_childrel->reltarget->width * fc_childrel->rows;

		/*
		 * 也累积每列的估算值。我们不需要对父列表中的 PlaceHolderVars 做任何事情。
		 * 如果子表达式不是 Var，或者我们没有记录它的宽度估算，我们必须回退到基于数据类型的估算。
		 *
		 * 按照构造，子查询的目标列表与父查询是 1 对 1 的。
		 */
		forboth(fc_parentvars, fc_rel->reltarget->exprs,
				fc_childvars, fc_childrel->reltarget->exprs)
		{
			Var		   *fc_parentvar = (Var *) lfirst(fc_parentvars);
			Node	   *fc_childvar = (Node *) lfirst(fc_childvars);

			if (IsA(fc_parentvar, Var) && fc_parentvar->varno == fc_parentRTindex)
			{
				int			fc_pndx = fc_parentvar->varattno - fc_rel->min_attr;
				int32		fc_child_width = 0;

				if (IsA(fc_childvar, Var) &&
					((Var *) fc_childvar)->varno == fc_childrel->relid)
				{
					int			fc_cndx = ((Var *) fc_childvar)->varattno - fc_childrel->min_attr;

					fc_child_width = fc_childrel->attr_widths[fc_cndx];
				}
				if (fc_child_width <= 0)
					fc_child_width = get_typavgwidth(exprType(fc_childvar),
												  exprTypmod(fc_childvar));
				Assert(fc_child_width > 0);
				fc_parent_attrsizes[fc_pndx] += fc_child_width * fc_childrel->rows;
			}
		}
	}

	if (fc_has_live_children)
	{
		/*
		 * 保存完成的大小估算。
		 */
		int			fc_i;

		Assert(fc_parent_rows > 0);
		fc_rel->rows = fc_parent_rows;
		fc_rel->reltarget->width = rint(fc_parent_size / fc_parent_rows);
		for (fc_i = 0; fc_i < fc_nattrs; fc_i++)
			fc_rel->attr_widths[fc_i] = rint(fc_parent_attrsizes[fc_i] / fc_parent_rows);

		/*
		 * 将“原始元组”计数设置为 appendrel 的“行”；这很必要，
		 * 因为某些地方假设 rel->tuples 对于任何基表都是有效的。
		 */
		fc_rel->tuples = fc_parent_rows;

		/*
		 * 请注意，我们将 rel->pages 留为零；这很重要，以避免在 total_table_pages 中
		 * 对 appendrel 树进行重复计数。
		 */
	}
	else
	{
		/*
		 * 所有子查询都被约束排除，因此将整个 appendrel 标记为虚拟。我们必须在此阶段执行此操作，
		 * 以使 rel 的虚拟性在为其他 rel 生成路径时可见。
		 */
		set_dummy_rel_pathlist(fc_rel);
	}

	pfree(fc_parent_attrsizes);
}

/*
 * set_append_rel_pathlist
 *	  为“追加关系”构建访问路径
 */
static void set_append_rel_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						Index fc_rti, RangeTblEntry *fc_rte)
{
	int			fc_parentRTindex = fc_rti;
	List	   *fc_live_childrels = NIL;
	ListCell   *fc_l;

	/*
	 * 为每个成员关系生成访问路径，并记住非虚拟的子查询。
	 */
	foreach(fc_l, fc_root->append_rel_list)
	{
		AppendRelInfo *fc_appinfo = (AppendRelInfo *) lfirst(fc_l);
		int			fc_childRTindex;
		RangeTblEntry *fc_childRTE;
		RelOptInfo *fc_childrel;

		/* append_rel_list包含所有追加关系；忽略其他 */
		if (fc_appinfo->parent_relid != fc_parentRTindex)
			continue;

		/* 重新定位子 RTE 和 RelOptInfo */
		fc_childRTindex = fc_appinfo->child_relid;
		fc_childRTE = fc_root->simple_rte_array[fc_childRTindex];
		fc_childrel = fc_root->simple_rel_array[fc_childRTindex];

		/*
		 * 如果 set_append_rel_size() 在访问此子查询后某个时刻决定父 appendrel
		 * 是并行不安全的，我们需要将不安全标记传播到子查询，以便我们不为其生成无用的部分路径。
		 */
		if (!fc_rel->consider_parallel)
			fc_childrel->consider_parallel = false;

		/*
		 * 计算子查询的访问路径。
		 */
		set_rel_pathlist(fc_root, fc_childrel, fc_childRTindex, fc_childRTE);

		/*
		 * 如果子查询是虚拟的，则忽略它。
		 */
		if (IS_DUMMY_REL(fc_childrel))
			continue;

		/*
		 * 子查询是活动的，因此将其添加到用于下面的 live_childrels 列表中。
		 */
		fc_live_childrels = lappend(fc_live_childrels, fc_childrel);
	}

	/* 将路径添加到追加关系。 */
	add_paths_to_append_rel(fc_root, fc_rel, fc_live_childrels);
}


/*
 * add_paths_to_append_rel
 *		为给定的追加关系生成路径，考虑非虚拟子查询的集合。
 *
 * 该函数收集所有非虚拟子查询支持的参数化和排序。对于每个这样的参数化或排序，它创建一个
 * 将每个非虚拟子查询的路径与给定参数化或排序相结合的追加路径。类似地，它收集非虚拟子查询的部分路径
 * 以创建部分追加路径。
 */
void add_paths_to_append_rel(PlannerInfo *fc_root, RelOptInfo *fc_rel,
						List *fc_live_childrels)
{
	List	   *fc_subpaths = NIL;
	bool		fc_subpaths_valid = true;
	List	   *fc_partial_subpaths = NIL;
	List	   *fc_pa_partial_subpaths = NIL;
	List	   *fc_pa_nonpartial_subpaths = NIL;
	bool		fc_partial_subpaths_valid = true;
	bool		fc_pa_subpaths_valid;
	List	   *fc_all_child_pathkeys = NIL;
	List	   *fc_all_child_outers = NIL;
	ListCell   *fc_l;
	double		fc_partial_rows = -1;

	/* 如果合适，考虑并行追加 */
	fc_pa_subpaths_valid = enable_parallel_append && fc_rel->consider_parallel;

	/*
	 * 对每个非虚拟子查询，记住最便宜的路径。此外，识别所有路径键（排序）
	 * 和参数化（required_outer 集合），这些对非虚拟成员关系是可用的。
	 */
	foreach(fc_l, fc_live_childrels)
	{
		RelOptInfo *fc_childrel = lfirst(fc_l);
		ListCell   *fc_lcp;
		Path	   *fc_cheapest_partial_path = NULL;

		/*
		 * 如果子查询具有未参数化的最便宜的总路径，将其添加到我们为父查询构建的未参数化的追加路径中。
		 * 如果没有，则不存在可用的未参数化路径。
		 *
		 * 对于分区聚合，子查询的路径列表可能是空的，因此不要假定此处存在路径。
		 */
		if (fc_childrel->pathlist != NIL &&
			fc_childrel->cheapest_total_path->param_info == NULL)
			fc_accumulate_append_subpath(fc_childrel->cheapest_total_path,
									  &fc_subpaths, NULL);
		else
			fc_subpaths_valid = false;

		/* 同样的想法，但用于部分计划。 */
		if (fc_childrel->partial_pathlist != NIL)
		{
			fc_cheapest_partial_path = linitial(fc_childrel->partial_pathlist);
			fc_accumulate_append_subpath(fc_cheapest_partial_path,
									  &fc_partial_subpaths, NULL);
		}
		else
			fc_partial_subpaths_valid = false;

		/*
		 * 同样的想法，但用于一个并行追加混合部分路径和非部分路径。
		 */
		if (fc_pa_subpaths_valid)
		{
			Path	   *fc_nppath = NULL;

			fc_nppath =
				get_cheapest_parallel_safe_total_inner(fc_childrel->pathlist);

			if (fc_cheapest_partial_path == NULL && fc_nppath == NULL)
			{
				/* 既不是部分路径也不是并行安全路径？忘记它。 */
				fc_pa_subpaths_valid = false;
			}
			else if (fc_nppath == NULL ||
					 (fc_cheapest_partial_path != NULL &&
					  fc_cheapest_partial_path->total_cost < fc_nppath->total_cost))
			{
				/* 部分路径更便宜或是唯一的选项。 */
				Assert(fc_cheapest_partial_path != NULL);
				fc_accumulate_append_subpath(fc_cheapest_partial_path,
										  &fc_pa_partial_subpaths,
										  &fc_pa_nonpartial_subpaths);
			}
			else
			{
				/*
				 * 要么我们只有一个非部分路径，要么我们认为单个后端能够比所有并行后端
				 * 一起执行最佳的非部分路径更快地执行最佳的非部分路径。
				 *
				 * 在这里更积极行动可能是合适的。即使最佳的非部分路径比最佳部分路径更昂贵，
				 * 但是如果有多个这样的路径，可以分配给不同的工作者，选择非部分路径可能仍然更好。
				 * 目前，我们不会尝试弄清楚这一点。
				 */
				fc_accumulate_append_subpath(fc_nppath,
										  &fc_pa_nonpartial_subpaths,
										  NULL);
			}
		}

		/*
		 * 收集所有可用路径排序和参数化的列表，以供所有子查询使用。我们将这些作为启发式规则，
		 * 来表明我们应该为哪些排序和参数化构建追加和合并追加路径。
		 */
		foreach(fc_lcp, fc_childrel->pathlist)
		{
			Path	   *fc_childpath = (Path *) lfirst(fc_lcp);
			List	   *fc_childkeys = fc_childpath->pathkeys;
			Relids		fc_childouter = PATH_REQ_OUTER(fc_childpath);

			/* 未排序路径不会对路径键列表有贡献 */
			if (fc_childkeys != NIL)
			{
				ListCell   *fc_lpk;
				bool		fc_found = false;

				/* 我们是否已经看到这种排序？ */
				foreach(fc_lpk, fc_all_child_pathkeys)
				{
					List	   *fc_existing_pathkeys = (List *) lfirst(fc_lpk);

					if (compare_pathkeys(fc_existing_pathkeys,
										 fc_childkeys) == PATHKEYS_EQUAL)
					{
						fc_found = true;
						break;
					}
				}
				if (!fc_found)
				{
					/* 不，所以将其添加到所有子路径键中 */
					fc_all_child_pathkeys = lappend(fc_all_child_pathkeys,
												 fc_childkeys);
				}
			}

			/* 未参数化的路径不参与参数集列表 */
			if (fc_childouter)
			{
				ListCell   *fc_lco;
				bool		fc_found = false;

				/* 我们已经看到这个参数集了吗？ */
				foreach(fc_lco, fc_all_child_outers)
				{
					Relids		fc_existing_outers = (Relids) lfirst(fc_lco);

					if (bms_equal(fc_existing_outers, fc_childouter))
					{
						fc_found = true;
						break;
					}
				}
				if (!fc_found)
				{
					/* 不，所以将其添加到所有子外部 */
					fc_all_child_outers = lappend(fc_all_child_outers,
											   fc_childouter);
				}
			}
		}
	}

	/*
	 * 如果我们为所有子项找到了未参数化的路径，则为关系构建一个无序的、
	 * 未参数化的 Append 路径。 （注意：即使由于约束排除我们有零个或一个活动的子路径，
	 * 这也是正确的。）
	 */
	if (fc_subpaths_valid)
		add_path(fc_rel, (Path *) create_append_path(fc_root, fc_rel, fc_subpaths, NIL,
												  NIL, NULL, 0, false,
												  -1));

	/*
	 * 考虑一个无序的、未参数化的部分路径的追加。 如果可能的话，使其支持并行。
	 */
	if (fc_partial_subpaths_valid && fc_partial_subpaths != NIL)
	{
		AppendPath *fc_appendpath;
		ListCell   *fc_lc;
		int			fc_parallel_workers = 0;

		/* 找到任何子路径请求的最高工作者数量。 */
		foreach(fc_lc, fc_partial_subpaths)
		{
			Path	   *fc_path = lfirst(fc_lc);

			fc_parallel_workers = Max(fc_parallel_workers, fc_path->parallel_workers);
		}
		Assert(fc_parallel_workers > 0);

		/*
		 * 如果允许使用并行追加，则始终请求至少 log2（子项数量）个工作者。
		 * 我们假设在这种情况下，拥有额外的工作者会很有用，因为它们将分布在
		 * 子项之间。 精确的公式只是一个猜测，但是我们不希望
		 * 在有 N 个分区的表和有相同数据的未分区表之间得到一个截然不同的答案，
		 * 因此在这里使用某种日志缩放似乎是有道理的。
		 */
		if (enable_parallel_append)
		{
			fc_parallel_workers = Max(fc_parallel_workers,
								   fls(list_length(fc_live_childrels)));
			fc_parallel_workers = Min(fc_parallel_workers,
								   max_parallel_workers_per_gather);
		}
		Assert(fc_parallel_workers > 0);

		/* 生成部分追加路径。 */
		fc_appendpath = create_append_path(fc_root, fc_rel, NIL, fc_partial_subpaths,
										NIL, NULL, fc_parallel_workers,
										enable_parallel_append,
										-1);

		/*
		 * 确保任何后续的部分路径使用相同的行数估计。
		 */
		fc_partial_rows = fc_appendpath->path.rows;

		/* 添加路径。 */
		add_partial_path(fc_rel, (Path *) fc_appendpath);
	}

	/*
	 * 考虑一个基于部分和非部分路径混合的支持并行的追加。
	 * （只有在至少有一个子项具有比任何部分路径便宜得多的非部分路径时
	 * 这才有意义；否则，我们应该使用前一步添加的追加路径。）
	 */
	if (fc_pa_subpaths_valid && fc_pa_nonpartial_subpaths != NIL)
	{
		AppendPath *fc_appendpath;
		ListCell   *fc_lc;
		int			fc_parallel_workers = 0;

		/*
		 * 找到任何部分子路径请求的最高工作者数量。
		 */
		foreach(fc_lc, fc_pa_partial_subpaths)
		{
			Path	   *fc_path = lfirst(fc_lc);

			fc_parallel_workers = Max(fc_parallel_workers, fc_path->parallel_workers);
		}

		/*
		 * 这里与上面的公式相同。 在这种情况下，更重要的是，因为非部分路径
		 * 对计划的并行工作者数量没有任何贡献。
		 */
		fc_parallel_workers = Max(fc_parallel_workers,
							   fls(list_length(fc_live_childrels)));
		fc_parallel_workers = Min(fc_parallel_workers,
							   max_parallel_workers_per_gather);
		Assert(fc_parallel_workers > 0);

		fc_appendpath = create_append_path(fc_root, fc_rel, fc_pa_nonpartial_subpaths,
										fc_pa_partial_subpaths,
										NIL, NULL, fc_parallel_workers, true,
										fc_partial_rows);
		add_partial_path(fc_rel, (Path *) fc_appendpath);
	}

	/*
	 * 还要基于收集到的子路径键列表构建未参数化的有序追加路径。
	 */
	if (fc_subpaths_valid)
		fc_generate_orderedappend_paths(fc_root, fc_rel, fc_live_childrels,
									 fc_all_child_pathkeys);

	/*
	 * 为在子关系中看到的每种参数化构建 Append 路径。
	 * （这看起来可能相当昂贵，但在大多数实际
	 * 相关的情况下，子关系将揭示大致相同的参数化，
	 * 所以实际上考虑的情况并不多。）
	 *
	 * Append 节点本身无法强制执行 quals，因此所有的 qual 检查必须
	 * 在子路径中完成。 这意味着要有一个参数化的
	 * Append 路径，我们必须为每个
	 * 子路径拥有完全相同的参数化；否则某些子项可能未能检查
	 * 下移的 quals。 为了使它们匹配，我们可以尝试提高
	 * 参数化较低的路径的参数化。
	 */
	foreach(fc_l, fc_all_child_outers)
	{
		Relids		fc_required_outer = (Relids) lfirst(fc_l);
		ListCell   *fc_lcr;

		/* 选择具有此参数化的追加的子路径 */
		fc_subpaths = NIL;
		fc_subpaths_valid = true;
		foreach(fc_lcr, fc_live_childrels)
		{
			RelOptInfo *fc_childrel = (RelOptInfo *) lfirst(fc_lcr);
			Path	   *fc_subpath;

			if (fc_childrel->pathlist == NIL)
			{
				/* 未能为这个子项创建合适的路径 */
				fc_subpaths_valid = false;
				break;
			}

			fc_subpath = get_cheapest_parameterized_child_path(fc_root,
															fc_childrel,
															fc_required_outer);
			if (fc_subpath == NULL)
			{
				/* 未能为这个子项创建合适的路径 */
				fc_subpaths_valid = false;
				break;
			}
			fc_accumulate_append_subpath(fc_subpath, &fc_subpaths, NULL);
		}

		if (fc_subpaths_valid)
			add_path(fc_rel, (Path *)
					 create_append_path(fc_root, fc_rel, fc_subpaths, NIL,
										NIL, fc_required_outer, 0, false,
										-1));
	}

	/*
	 * 当只有一个子关系时，追加路径可以继承
	 * 子关系路径的任何可用排序，因此考虑有序部分路径是有用的。
	 * 上面我们只考虑了每个子项的最便宜部分路径，但我们也要使用
	 * 任何具有路径键的部分路径来创建路径。
	 */
	if (list_length(fc_live_childrels) == 1)
	{
		RelOptInfo *fc_childrel = (RelOptInfo *) linitial(fc_live_childrels);

		/* 跳过最便宜的部分路径，因为我们已经在上面使用过了 */
		for_each_from(fc_l, fc_childrel->partial_pathlist, 1)
		{
			Path	   *fc_path = (Path *) lfirst(fc_l);
			AppendPath *fc_appendpath;

			/* 跳过没有路径键的路径。 */
			if (fc_path->pathkeys == NIL)
				continue;

			fc_appendpath = create_append_path(fc_root, fc_rel, NIL, list_make1(fc_path),
											NIL, NULL,
											fc_path->parallel_workers, true,
											fc_partial_rows);
			add_partial_path(fc_rel, (Path *) fc_appendpath);
		}
	}
}

/*
 * generate_orderedappend_paths
 *		为追加关系生成有序追加路径
 *
 * 通常我们在这里生成 MergeAppend 路径，但在某些特殊
 * 情况下我们可以生成简单的 Append 路径，因为子路径
 * 可以提供所需顺序的元组。
 *
 * 我们为所有_child_pathkeys 中出现的每个排序（路径键列表）生成路径。
 *
 * 我们考虑最便宜的启动和最便宜的总情况，即，对于每个
 * 有趣的排序，收集所有最便宜的启动子路径和所有
 * 最便宜的总路径，并为每种情况构建合适的路径。
 *
 * 我们目前不在这里生成任何参数化的有序路径。
 * 虽然在这里生成这样的路径几乎不需要更多的代码，
 * 但很不清楚值得耗费计划周期来调查这样的路径：
 * 在嵌套循环内部使用有序路径几乎没有意义。
 * 事实上，当前的add_path编码极有可能会直接拒绝这样的路径，
 * 因为add_path对参数化路径的排序没有给予任何信用，
 * 并且参数化的 MergeAppend 将比相应的参数化 Append 路径更昂贵。
 * 如果我们将来努力支持参数化合并连接计划，
 * 可能值得在这里增加对参数化路径的支持以便用于这样的连接。
 * （有关为什么这可能永远不会发生的说明，请参见
 * optimizer/README。）
 */
static void fc_generate_orderedappend_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							 List *fc_live_childrels,
							 List *fc_all_child_pathkeys)
{
	ListCell   *fc_lcp;
	List	   *fc_partition_pathkeys = NIL;
	List	   *fc_partition_pathkeys_desc = NIL;
	bool		fc_partition_pathkeys_partial = true;
	bool		fc_partition_pathkeys_desc_partial = true;

	
/*
	 * 一些分区表设置可能允许我们使用 Append 节点而不是 MergeAppend。
	 * 这种情况适用于 RANGE 分区表，在这种情况下，可以保证早期的分区必须包含
	 * 在排序顺序中较早的行。要检测这是否相关，请构建分区排序的路径键描述，
	 * 以用于正向和反向扫描。
	 */
	if (fc_rel->part_scheme != NULL && IS_SIMPLE_REL(fc_rel) &&
		partitions_are_ordered(fc_rel->boundinfo, fc_rel->live_parts))
	{
		fc_partition_pathkeys = build_partition_pathkeys(fc_root, fc_rel,
													  ForwardScanDirection,
													  &fc_partition_pathkeys_partial);

		fc_partition_pathkeys_desc = build_partition_pathkeys(fc_root, fc_rel,
														   BackwardScanDirection,
														   &fc_partition_pathkeys_desc_partial);

		/*
		 * 你可能会认为我们应该在这里截断无用的路径键，但
		 * 允许作为查询路径键子集的分区键往往是有用的。
		 * 例如，考虑一个通过 RANGE (a, b) 分区的表，以及
		 * 一个带有 ORDER BY a, b, c 的查询。如果我们有子路径可以生成
		 * a, b, c 的排序（可能通过 (a, b, c) 上的索引），
		 * 那么考虑 appendrel 输出按 a, b, c 排序是可行的。
		 */
	}

	/* 现在考虑每个有趣的排序顺序 */
	foreach(fc_lcp, fc_all_child_pathkeys)
	{
		List	   *fc_pathkeys = (List *) lfirst(fc_lcp);
		List	   *fc_startup_subpaths = NIL;
		List	   *fc_total_subpaths = NIL;
		List	   *fc_fractional_subpaths = NIL;
		bool		fc_startup_neq_total = false;
		ListCell   *fc_lcr;
		bool		fc_match_partition_order;
		bool		fc_match_partition_order_desc;

		/*
		 * 确定该排序顺序是否与我们拥有的任何分区路径键匹配，
		 * 包括升序和降序分区顺序。如果分区路径键恰好包含在路径键中，
		 * 那么它仍然有效，如上所述，前提是分区路径键是完整的而不仅仅是
		 * 分区键的前缀。 (在这种情况下，我们将依赖子路径对所需的路径键
		 * 的低阶列进行排序。)
		 */
		fc_match_partition_order =
			pathkeys_contained_in(fc_pathkeys, fc_partition_pathkeys) ||
			(!fc_partition_pathkeys_partial &&
			 pathkeys_contained_in(fc_partition_pathkeys, fc_pathkeys));

		fc_match_partition_order_desc = !fc_match_partition_order &&
			(pathkeys_contained_in(fc_pathkeys, fc_partition_pathkeys_desc) ||
			 (!fc_partition_pathkeys_desc_partial &&
			  pathkeys_contained_in(fc_partition_pathkeys_desc, fc_pathkeys)));

		/* 选择该排序的子路径... */
		foreach(fc_lcr, fc_live_childrels)
		{
			RelOptInfo *fc_childrel = (RelOptInfo *) lfirst(fc_lcr);
			Path	   *fc_cheapest_startup,
					   *fc_cheapest_total,
					   *fc_cheapest_fractional = NULL;

			/* 找到正确的路径，如果它们可用。 */
			fc_cheapest_startup =
				get_cheapest_path_for_pathkeys(fc_childrel->pathlist,
											   fc_pathkeys,
											   NULL,
											   STARTUP_COST,
											   false);
			fc_cheapest_total =
				get_cheapest_path_for_pathkeys(fc_childrel->pathlist,
											   fc_pathkeys,
											   NULL,
											   TOTAL_COST,
											   false);

			/*
			 * 如果我们找不到任何具有正确顺序的路径，就使用
			 * 最便宜的总路径；我们将不得不在后面对其进行排序。
			 */
			if (fc_cheapest_startup == NULL || fc_cheapest_total == NULL)
			{
				fc_cheapest_startup = fc_cheapest_total =
					fc_childrel->cheapest_total_path;
				/* 断言我们确实有一个无参数化的路径用于这个子路径 */
				Assert(fc_cheapest_total->param_info == NULL);
			}

			/*
			 * 在构建分数路径时, 还需要为每个子关系确定一个便宜的
			 * 分数路径。仅查看启动和总成本是不够的，因为最便宜的分数
			 * 路径可能会被两个独立的路径（一个用于启动，一个用于总计）所主导。
			 *
			 * 如果需要（构建分数路径），也要确定最便宜的
			 * 分数路径。
			 */
			if (fc_root->tuple_fraction > 0)
			{
				double		fc_path_fraction = (1.0 / fc_root->tuple_fraction);

				fc_cheapest_fractional =
					get_cheapest_fractional_path_for_pathkeys(fc_childrel->pathlist,
															  fc_pathkeys,
															  NULL,
															  fc_path_fraction);

				/*
				 * 如果未找到与路径键匹配的路径，则使用
				 * 最便宜的总路径。
				 *
				 * XXX 我们可能还考虑部分排序路径（在顶部增量排序）。
				 * 但我们必须构建所有增量路径，做成本计算等。
				 */
				if (!fc_cheapest_fractional)
					fc_cheapest_fractional = fc_cheapest_total;
			}

			/*
			 * 注意我们是否实际上对“最便宜”和“总计”情况
			 * 有不同的路径；通常没有必要进行两个 create_merge_append_path() 调用。
			 */
			if (fc_cheapest_startup != fc_cheapest_total)
				fc_startup_neq_total = true;

			/*
			 * 收集适当的子路径。所需的逻辑因 Append 和 MergeAppend
			 * 情况而异。
			 */
			if (fc_match_partition_order)
			{
				/*
				 * 我们将创建一个普通的 Append 路径。我们不需要
				 * accumulate_append_subpath 的大部分功能，但我们希望
				 * 在子 Append 或 MergeAppend 仅有一个子路径时将其排除
				 * （因此没有执行任何有用的操作）。
				 */
				fc_cheapest_startup = get_singleton_append_subpath(fc_cheapest_startup);
				fc_cheapest_total = get_singleton_append_subpath(fc_cheapest_total);

				fc_startup_subpaths = lappend(fc_startup_subpaths, fc_cheapest_startup);
				fc_total_subpaths = lappend(fc_total_subpaths, fc_cheapest_total);

				if (fc_cheapest_fractional)
				{
					fc_cheapest_fractional = get_singleton_append_subpath(fc_cheapest_fractional);
					fc_fractional_subpaths = lappend(fc_fractional_subpaths, fc_cheapest_fractional);
				}
			}
			else if (fc_match_partition_order_desc)
			{
				/*
				 * 如上所述，但我们需要逆转子节点的顺序，
				 * 因为 nodeAppend.c 对逆序一无所知，将按照提供的顺序扫描子节点。
				 */
				fc_cheapest_startup = get_singleton_append_subpath(fc_cheapest_startup);
				fc_cheapest_total = get_singleton_append_subpath(fc_cheapest_total);

				fc_startup_subpaths = lcons(fc_cheapest_startup, fc_startup_subpaths);
				fc_total_subpaths = lcons(fc_cheapest_total, fc_total_subpaths);

				if (fc_cheapest_fractional)
				{
					fc_cheapest_fractional = get_singleton_append_subpath(fc_cheapest_fractional);
					fc_fractional_subpaths = lcons(fc_cheapest_fractional, fc_fractional_subpaths);
				}
			}
			else
			{
				/*
				 * 否则，依赖于 accumulate_append_subpath 收集
				 * MergeAppend 的子路径。
				 */
				fc_accumulate_append_subpath(fc_cheapest_startup,
										  &fc_startup_subpaths, NULL);
				fc_accumulate_append_subpath(fc_cheapest_total,
										  &fc_total_subpaths, NULL);

				if (fc_cheapest_fractional)
					fc_accumulate_append_subpath(fc_cheapest_fractional,
											  &fc_fractional_subpaths, NULL);
			}
		}

		/* ... 并构建 Append 或 MergeAppend 路径 */
		if (fc_match_partition_order || fc_match_partition_order_desc)
		{
			/* 我们只需要 Append */
			add_path(fc_rel, (Path *) create_append_path(fc_root,
													  fc_rel,
													  fc_startup_subpaths,
													  NIL,
													  fc_pathkeys,
													  NULL,
													  0,
													  false,
													  -1));
			if (fc_startup_neq_total)
				add_path(fc_rel, (Path *) create_append_path(fc_root,
														  fc_rel,
														  fc_total_subpaths,
														  NIL,
														  fc_pathkeys,
														  NULL,
														  0,
														  false,
														  -1));

			if (fc_fractional_subpaths)
				add_path(fc_rel, (Path *) create_append_path(fc_root,
														  fc_rel,
														  fc_fractional_subpaths,
														  NIL,
														  fc_pathkeys,
														  NULL,
														  0,
														  false,
														  -1));
		}
		else
		{
			/* 我们需要 MergeAppend */
			add_path(fc_rel, (Path *) create_merge_append_path(fc_root,
															fc_rel,
															fc_startup_subpaths,
															fc_pathkeys,
															NULL));
			if (fc_startup_neq_total)
				add_path(fc_rel, (Path *) create_merge_append_path(fc_root,
																fc_rel,
																fc_total_subpaths,
																fc_pathkeys,
																NULL));

			if (fc_fractional_subpaths)
				add_path(fc_rel, (Path *) create_merge_append_path(fc_root,
																fc_rel,
																fc_fractional_subpaths,
																fc_pathkeys,
																NULL));
		}
	}
}

/*
 * get_cheapest_parameterized_child_path
 *		获取具有精确所需参数化的此关系的最便宜路径。
 *
 * 如果无法创建此类路径，则返回 NULL。
 */
static Path * get_cheapest_parameterized_child_path(PlannerInfo *fc_root, RelOptInfo *fc_rel,
									  Relids fc_required_outer)
{
	Path	   *fc_cheapest;
	ListCell   *fc_lc;

	/*
	 * 查找具有不超过所需参数化的最便宜现有路径。
	 * 如果它恰好具有所需的参数化，我们就完成了。
	 */
	fc_cheapest = get_cheapest_path_for_pathkeys(fc_rel->pathlist,
											  NIL,
											  fc_required_outer,
											  TOTAL_COST,
											  false);
	Assert(fc_cheapest != NULL);
	if (bms_equal(PATH_REQ_OUTER(fc_cheapest), fc_required_outer))
		return fc_cheapest;

	/*
	 * 否则，我们可以“重新参数化”一个现有路径以匹配给定的
	 * 参数化，这实际上意味着将额外的 joinquals 向下推送，以便在路径的扫描中进行检查。
	 * 然而，有些现有路径可能已经检查可用的 joinquals，而其他则没有；
	 * 因此，不清楚重新参数化后哪个现有路径会是最便宜的。
	 * 我们必须逐个检查以找出。
	 */
	fc_cheapest = NULL;
	foreach(fc_lc, fc_rel->pathlist)
	{
		Path	   *fc_path = (Path *) lfirst(fc_lc);

		/* 如果需要的参数化超过请求的，则无法使用 */
		if (!bms_is_subset(PATH_REQ_OUTER(fc_path), fc_required_outer))
			continue;

		/*
		 * 重新参数化只会增加路径的成本，因此如果它已经
		 * 贵于当前最便宜的，则忘掉它。
		 */
		if (fc_cheapest != NULL &&
			compare_path_costs(fc_cheapest, fc_path, TOTAL_COST) <= 0)
			continue;

		/* 如果需要，重新参数化，然后重新检查成本 */
		if (!bms_equal(PATH_REQ_OUTER(fc_path), fc_required_outer))
		{
			fc_path = reparameterize_path(fc_root, fc_path, fc_required_outer, 1.0);
			if (fc_path == NULL)
				continue;		/* 无法重新参数化这个 */
			Assert(bms_equal(PATH_REQ_OUTER(fc_path), fc_required_outer));

			if (fc_cheapest != NULL &&
				compare_path_costs(fc_cheapest, fc_path, TOTAL_COST) <= 0)
				continue;
		}

		/* 我们有一个新的最佳路径 */
		fc_cheapest = fc_path;
	}

	/* 返回最佳路径，或者如果未找到合适的候选者则返回 NULL */
	return fc_cheapest;
}

/*
 * accumulate_append_subpath
 *		将子路径添加到为 Append 或 MergeAppend 构建的列表中。
 *
 * 子路径可能本身就是 Append 或 MergeAppend 路径，在
 * 这种情况下，我们可以“省略中间人”，只需将其子路径添加到
 * 我们自己的列表中。（我们不早些尝试这样做，因为我们需要
 *对 quals 应用两个级别的转换。）
 *
 * 请注意，如果以这种方式省略子 MergeAppend，则有效地
 * 省略了排序步骤，这似乎没问题：如果父节点是 Append，
 * 则其结果无论如何会是未排序的，而如果父节点是 MergeAppend，
 * 则对子节点的单独排序没有意义。
 *
 * 通常，任一路径都是部分路径，而 subpaths 是部分路径的列表，
 * 或者路径是非部分计划，而 subpaths 是这些的列表。
 * 然而，如果路径是并行感知的 Append，则我们将其部分路径
 * 子节点添加到 subpaths，其余添加到 special_subpaths。
 * 如果后者为 NULL，我们根本不扁平化路径
 * （除非它只包含部分路径）。
 */
static void fc_accumulate_append_subpath(Path *fc_path, List **fc_subpaths, List **fc_special_subpaths)
{
	if (IsA(fc_path, AppendPath))
	{
		AppendPath *fc_apath = (AppendPath *) fc_path;

		if (!fc_apath->path.parallel_aware || fc_apath->first_partial_path == 0)
		{
			*fc_subpaths = list_concat(*fc_subpaths, fc_apath->subpaths);
			return;
		}
		else if (fc_special_subpaths != NULL)
		{
			List	   *fc_new_special_subpaths;

			/* 将并行 Append 拆分为部分和非部分子路径 */
			*fc_subpaths = list_concat(*fc_subpaths,
									list_copy_tail(fc_apath->subpaths,
												   fc_apath->first_partial_path));
			fc_new_special_subpaths =
				list_truncate(list_copy(fc_apath->subpaths),
							  fc_apath->first_partial_path);
			*fc_special_subpaths = list_concat(*fc_special_subpaths,
											fc_new_special_subpaths);
			return;
		}
	}
	else if (IsA(fc_path, MergeAppendPath))
	{
		MergeAppendPath *fc_mpath = (MergeAppendPath *) fc_path;

		*fc_subpaths = list_concat(*fc_subpaths, fc_mpath->subpaths);
		return;
	}

	*fc_subpaths = lappend(*fc_subpaths, fc_path);
}

/*
 * get_singleton_append_subpath
 *		返回 Append/MergeAppend 的唯一子路径，或仅仅
 *		返回 'path' 如果它不是单子路径 Append/MergeAppend。
 *
 * 注意：'path' 不能是并行感知路径。
 */
static Path * get_singleton_append_subpath(Path *fc_path)
{
	Assert(!fc_path->parallel_aware);

	if (IsA(fc_path, AppendPath))
	{
		AppendPath *fc_apath = (AppendPath *) fc_path;

		if (list_length(fc_apath->subpaths) == 1)
			return (Path *) linitial(fc_apath->subpaths);
	}
	else if (IsA(fc_path, MergeAppendPath))
	{
		MergeAppendPath *fc_mpath = (MergeAppendPath *) fc_path;

		if (list_length(fc_mpath->subpaths) == 1)
			return (Path *) linitial(fc_mpath->subpaths);
	}

	return fc_path;
}

/*
 * set_dummy_rel_pathlist
 *	  为因约束而被排除的关系构建一个虚拟路径
 *
 * 与其发明一种特殊的“虚拟”路径类型，不如将其表示为
 * 没有成员的 AppendPath（另见 IS_DUMMY_APPEND/IS_DUMMY_REL 宏）。
 *
 * （另见 mark_dummy_rel，它执行基本相同的操作，但通常用于在
 * 我们已经为其创建路径后将关系更改为虚拟状态。）
 */
static void set_dummy_rel_pathlist(RelOptInfo *fc_rel)
{
	
/* 设置虚拟大小估算 --- 我们将 attr_widths[] 留为零 */
	fc_rel->rows = 0;
	fc_rel->reltarget->width = 0;

	/* 丢弃任何预先存在的路径；不再需要它们 */
	fc_rel->pathlist = NIL;
	fc_rel->partial_pathlist = NIL;

	/* 设置虚拟路径 */
	add_path(fc_rel, (Path *) create_append_path(NULL, fc_rel, NIL, NIL,
											  NIL, fc_rel->lateral_relids,
											  0, false, -1));

	/*
	 * 我们立即设置最便宜路径字段，以防它们指向某些丢弃的路径。
	 * 当我们从 set_rel_size() 被调用时这很冗余，但从其他地方
	 * 被调用时就不是，而且无论如何这样做两次也无妨。
	 */
	set_cheapest(fc_rel);
}

/* 快速且粗略的测试以查看是否需要任何连接 */
static bool fc_has_multiple_baserels(PlannerInfo *fc_root)
{
	int			fc_num_base_rels = 0;
	Index		fc_rti;

	for (fc_rti = 1; fc_rti < fc_root->simple_rel_array_size; fc_rti++)
	{
		RelOptInfo *fc_brel = fc_root->simple_rel_array[fc_rti];

		if (fc_brel == NULL)
			continue;

		/* 忽略被视为“其他关系”的RTE */
		if (fc_brel->reloptkind == RELOPT_BASEREL)
			if (++fc_num_base_rels > 1)
				return true;
	}
	return false;
}

/*
 * find_window_run_conditions
 *		确定 'wfunc' 是否真的为 WindowFunc，并调用其 prosupport
 *		函数以确定该函数的单调属性。然后我们查看 'opexpr'
 *		是否可以用于短路执行。
 *
 * 例如 row_number() over (order by ...) 始终产生一个比之前高的值。
 * 如果在子查询中有一个窗口函数，并且在外部查询中有一个 WHERE 子句
 * 用于过滤行 <= 10，则我们可能会在行号达到 11 后停止处理 
 * windowagg。在这里我们检查 'opexpr' 是否可能帮助我们停止在 
 * WindowAgg 节点中进行不必要的额外处理。
 *
 * '*keep_original' 被设置为 true 如果调用者还应该将 'opexpr' 用于
 * 其原始目的。如果调用者可以假设运行条件将处理所有必需的过滤，
 * 则设置为 false。
 *
 * 如果发现 'opexpr' 是有用的并已添加到 WindowClauses runCondition，
 * 则返回 true。我们还相应地设置 *keep_original，并将 'attno' 加入
 * *run_cond_attrs，偏移 FirstLowInvalidHeapAttributeNumber。
 * 如果 'opexpr' 不能使用，则将 *keep_original 设置为 true 并返回 false。
 */
static bool fc_find_window_run_conditions(Query *fc_subquery, RangeTblEntry *fc_rte, Index fc_rti,
						   AttrNumber fc_attno, WindowFunc *fc_wfunc, OpExpr *fc_opexpr,
						   bool fc_wfunc_left, bool *fc_keep_original,
						   Bitmapset **fc_run_cond_attrs)
{
	Oid			fc_prosupport;
	Expr	   *fc_otherexpr;
	SupportRequestWFuncMonotonic fc_req;
	SupportRequestWFuncMonotonic *fc_res;
	WindowClause *fc_wclause;
	List	   *fc_opinfos;
	OpExpr	   *fc_runopexpr;
	Oid			fc_runoperator;
	ListCell   *fc_lc;

	*fc_keep_original = true;

	while (IsA(fc_wfunc, RelabelType))
		fc_wfunc = (WindowFunc *) ((RelabelType *) fc_wfunc)->arg;

	/* 我们只能处理窗口函数 */
	if (!IsA(fc_wfunc, WindowFunc))
		return false;

	/* 如果窗口函数中有子计划则无法使用 */
	if (contain_subplans((Node *) fc_wfunc))
		return false;

	fc_prosupport = get_func_support(fc_wfunc->winfnoid);

	/* 检查是否有'wfunc'的支持函数 */
	if (!OidIsValid(fc_prosupport))
		return false;

	/* 从OpExpr的另一侧获取Expr */
	if (fc_wfunc_left)
		fc_otherexpr = lsecond(fc_opexpr->args);
	else
		fc_otherexpr = linitial(fc_opexpr->args);

	/*
	 * 被比较的值在窗口划分的评估过程中必须保持不变。
	 */
	if (!is_pseudo_constant_clause((Node *) fc_otherexpr))
		return false;

	/* 找到属于窗口函数的窗口子句 */
	fc_wclause = (WindowClause *) list_nth(fc_subquery->windowClause,
										fc_wfunc->winref - 1);

	fc_req.type = T_SupportRequestWFuncMonotonic;
	fc_req.window_func = fc_wfunc;
	fc_req.window_clause = fc_wclause;

	/* 调用支持函数 */
	fc_res = (SupportRequestWFuncMonotonic *)
		DatumGetPointer(OidFunctionCall1(fc_prosupport,
										 PointerGetDatum(&fc_req)));

	/*
	 * 如果函数既不是单调递增也不是单调递减，则无需执行任何操作。
	 */
	if (fc_res == NULL || fc_res->monotonic == MONOTONICFUNC_NONE)
		return false;

	fc_runopexpr = NULL;
	fc_runoperator = InvalidOid;
	fc_opinfos = get_op_btree_interpretation(fc_opexpr->opno);

	foreach(fc_lc, fc_opinfos)
	{
		OpBtreeInterpretation *fc_opinfo = (OpBtreeInterpretation *) lfirst(fc_lc);
		int			fc_strategy = fc_opinfo->strategy;

		/* 处理 < / <= */
		if (fc_strategy == BTLessStrategyNumber ||
			fc_strategy == BTLessEqualStrategyNumber)
		{
			/*
			 * < / <= 在单调递增函数的情况下以<wfunc> op <pseudoconst>和<pseudoconst> op <wfunc>
			 * 的形式得到支持，适用于单调递减函数。
			 */
			if ((fc_wfunc_left && (fc_res->monotonic & MONOTONICFUNC_INCREASING)) ||
				(!fc_wfunc_left && (fc_res->monotonic & MONOTONICFUNC_DECREASING)))
			{
				*fc_keep_original = false;
				fc_runopexpr = fc_opexpr;
				fc_runoperator = fc_opexpr->opno;
			}
			break;
		}
		/* 处理 > / >= */
		else if (fc_strategy == BTGreaterStrategyNumber ||
				 fc_strategy == BTGreaterEqualStrategyNumber)
		{
			/*
			 * > / >= 在单调递减函数的情况下以<wfunc> op <pseudoconst>和<pseudoconst> op <wfunc>
			 * 的形式得到支持，适用于单调递增函数。
			 */
			if ((fc_wfunc_left && (fc_res->monotonic & MONOTONICFUNC_DECREASING)) ||
				(!fc_wfunc_left && (fc_res->monotonic & MONOTONICFUNC_INCREASING)))
			{
				*fc_keep_original = false;
				fc_runopexpr = fc_opexpr;
				fc_runoperator = fc_opexpr->opno;
			}
			break;
		}
		/* handle = */
		else if (fc_strategy == BTEqualStrategyNumber)
		{
			int16		fc_newstrategy;

			/*
			 * 当既单调递增又单调递减时，窗口函数的返回值将每次都是相同的。
			 * 我们可以简单地将'opexpr'用作运行条件，而无需修改它。
			 */
			if ((fc_res->monotonic & MONOTONICFUNC_BOTH) == MONOTONICFUNC_BOTH)
			{
				*fc_keep_original = false;
				fc_runopexpr = fc_opexpr;
				fc_runoperator = fc_opexpr->opno;
				break;
			}

			/*
			 * 当单调递增时，我们使用<wfunc> <= <value>或<value> >= <wfunc>构建一个qual
			 * 以过滤掉在相等条件中高于该值的值。对于单调递减函数，我们想要过滤掉在相等
			 * 条件中低于该值的值。
			 */
			if (fc_res->monotonic & MONOTONICFUNC_INCREASING)
				fc_newstrategy = fc_wfunc_left ? BTLessEqualStrategyNumber : BTGreaterEqualStrategyNumber;
			else
				fc_newstrategy = fc_wfunc_left ? BTGreaterEqualStrategyNumber : BTLessEqualStrategyNumber;

			/* 我们必须保持原始的相等qual */
			*fc_keep_original = true;
			fc_runopexpr = fc_opexpr;

			/* 确定用于runCondition qual的运算符 */
			fc_runoperator = get_opfamily_member(fc_opinfo->opfamily_id,
											  fc_opinfo->oplefttype,
											  fc_opinfo->oprighttype,
											  fc_newstrategy);
			break;
		}
	}

	if (fc_runopexpr != NULL)
	{
		Expr	   *fc_newexpr;

		/*
		 * 构建所需的qual以满足运行条件，并将WindowFunc保持在其原始侧。
		 */
		if (fc_wfunc_left)
			fc_newexpr = make_opclause(fc_runoperator,
									fc_runopexpr->opresulttype,
									fc_runopexpr->opretset, (Expr *) fc_wfunc,
									fc_otherexpr, fc_runopexpr->opcollid,
									fc_runopexpr->inputcollid);
		else
			fc_newexpr = make_opclause(fc_runoperator,
									fc_runopexpr->opresulttype,
									fc_runopexpr->opretset,
									fc_otherexpr, (Expr *) fc_wfunc,
									fc_runopexpr->opcollid,
									fc_runopexpr->inputcollid);

		fc_wclause->runCondition = lappend(fc_wclause->runCondition, fc_newexpr);

		/* 记录此attno在运行条件中被使用 */
		*fc_run_cond_attrs = bms_add_member(*fc_run_cond_attrs,
										 fc_attno - FirstLowInvalidHeapAttributeNumber);
		return true;
	}

	/* 不支持的OpExpr */
	return false;
}

/*
 * check_and_push_window_quals
 *		检查'clause'是否是可以作为'runCondition' qual推入WindowFunc的
 *		WindowClause的qual。 当存在这些时，在执行期间可以跳过
 *		一些不必要的工作。
 *
 * 'run_cond_attrs'将填充所有子查询目标的targetlist resnos
 * （偏移量为FirstLowInvalidHeapAttributeNumber），我们为其推送
 * 窗口qual。
 *
 * 如果调用者仍然必须保持原始qual，则返回true；如果调用者
 * 可以安全地忽略原始qual，因为WindowAgg节点将使用runCondition
 * 停止返回元组，则返回false。
 */
static bool fc_check_and_push_window_quals(Query *fc_subquery, RangeTblEntry *fc_rte, Index fc_rti,
							Node *fc_clause, Bitmapset **fc_run_cond_attrs)
{
	OpExpr	   *fc_opexpr = (OpExpr *) fc_clause;
	bool		fc_keep_original = true;
	Var		   *fc_var1;
	Var		   *fc_var2;

	/* 我们只能使用具有2个操作数的OpExpr */
	if (!IsA(fc_opexpr, OpExpr))
		return true;

	if (list_length(fc_opexpr->args) != 2)
		return true;

	/*
	 * 当前，我们将此优化限制为严格的OpExprs。这样做的原因在于
	 * 在执行过程中，一旦runcondition变为false，我们就停止评估
	 * WindowFuncs。为了避免留下过时的窗口函数结果值，我们将其设置为NULL。
	 * 在此仅包含严格的OpExprs可以确保我们在顶层WindowAgg中妥善过滤掉NULLs的元组。
	 */
	set_opfuncid(fc_opexpr);
	if (!func_strict(fc_opexpr->opfuncid))
		return true;

	/*
	 * 检查在子查询中引用窗口函数的普通Var。
	 * 如果发现任何，我们将请求find_window_run_conditions()检查'opexpr'
	 * 是否可以作为运行条件的一部分使用。
	 */

	/* 检查OpExpr的左侧 */
	fc_var1 = linitial(fc_opexpr->args);
	if (IsA(fc_var1, Var) && fc_var1->varattno > 0)
	{
		TargetEntry *fc_tle = list_nth(fc_subquery->targetList, fc_var1->varattno - 1);
		WindowFunc *fc_wfunc = (WindowFunc *) fc_tle->expr;

		if (fc_find_window_run_conditions(fc_subquery, fc_rte, fc_rti, fc_tle->resno, fc_wfunc,
									   fc_opexpr, true, &fc_keep_original,
									   fc_run_cond_attrs))
			return fc_keep_original;
	}

	/* 并检查右侧 */
	fc_var2 = lsecond(fc_opexpr->args);
	if (IsA(fc_var2, Var) && fc_var2->varattno > 0)
	{
		TargetEntry *fc_tle = list_nth(fc_subquery->targetList, fc_var2->varattno - 1);
		WindowFunc *fc_wfunc = (WindowFunc *) fc_tle->expr;

		if (fc_find_window_run_conditions(fc_subquery, fc_rte, fc_rti, fc_tle->resno, fc_wfunc,
									   fc_opexpr, false, &fc_keep_original,
									   fc_run_cond_attrs))
			return fc_keep_original;
	}

	return true;
}

/*
 * set_subquery_pathlist
 *		为子查询RTE生成SubqueryScan访问路径
 *
 * 我们当前不支持通过将连接子句推送到子查询中来生成带参数的路径；
 * 重新规划子查询以考虑不同替代方案似乎代价过高。
 * （XXX 这可能需要重新考虑，因为我们现在使用Paths。）
 * 因此，制作的路径将在子查询包含LATERAL引用时被参数化，
 * 否则则不会。只要满足这一点，就不需要单独的set_subquery_size阶段：
 * 只需立即制作路径。
 */
static void set_subquery_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
					  Index fc_rti, RangeTblEntry *fc_rte)
{
	Query	   *fc_parse = fc_root->parse;
	Query	   *fc_subquery = fc_rte->subquery;
	Relids		fc_required_outer;
	pushdown_safety_info fc_safetyInfo;
	double		fc_tuple_fraction;
	RelOptInfo *fc_sub_final_rel;
	Bitmapset  *fc_run_cond_attrs = NULL;
	ListCell   *fc_lc;

	/*
	 * 必须复制Query，以便规划不会搞乱RTE内容
	 * （真的非常需要修复规划器，不要在其输入上乱写，
	 * 有一天……但请参见remove_unused_subquery_outputs开始处理）。
	 */
	fc_subquery = copyObject(fc_subquery);

	/*
	 * 如果这是一个LATERAL子查询，它可能包含一些当前查询级别的Var，
	 * 需要将其视为带参数的，即使我们不支持将连接qual推送到子查询中。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/*
	 * 将subquery_is_pushdown_safe的结果区域清零，以便在递归时根据需要设置标志。
	 * 特别是，我们需要一个工作区来跟踪为什么列引用不安全的原因。
	 * 这些原因在我们发现子查询的列i不安全可以在推送下的qual中使用时存储在
	 * unsafeFlags[i]中的位。
	 */
	memset(&fc_safetyInfo, 0, sizeof(fc_safetyInfo));
	fc_safetyInfo.unsafeFlags = (unsigned char *)
		palloc0((list_length(fc_subquery->targetList) + 1) * sizeof(unsigned char));

	
/*
	 * 如果子查询具有“security_barrier”标志，则意味着子查询
	 * 源自必须执行行级安全性的视图。那么我们
	 * 不能推送包含泄漏函数的条件。 （理想情况下，这个检查应该在 subquery_is_pushdown_safe 中进行，但由于我们当前不向该函数传递 RTE，因此必须在这里进行。）
	 */
	fc_safetyInfo.unsafeLeaky = fc_rte->security_barrier;

	/*
	 * 如果有任何限制子句已附加到
	 * 子查询关系，请考虑将它们推送下来，成为子查询本身的 WHERE 或 HAVING
	 * 条件。此转换是有用的，因为它
	 * 可能允许我们为子查询生成比评估
	 * 所有子查询输出行然后过滤它们更好的计划。
	 *
	 * 有几种情况我们无法推送下去子句。涉及子查询的限制
	 * 由 subquery_is_pushdown_safe() 检查。
	 * 对单个子句的限制由
	 * qual_is_pushdown_safe() 检查。此外，我们不想推送下去
	 * 伪常量子句；最好让门控节点位于
	 * 子查询之上。
	 *
	 * 不被推送的子句将作为
	 * SubqueryScan 节点的 qpquals 进行评估。
	 *
	 * XXX 是否有任何情况我们想做出政策决定不推送一个可推送的条件，因为这会导致一个更差的计划？
	 */
	if (fc_rel->baserestrictinfo != NIL &&
		fc_subquery_is_pushdown_safe(fc_subquery, fc_subquery, &fc_safetyInfo))
	{
		/* 可以考虑将单个条件下推 */
		List	   *fc_upperrestrictlist = NIL;
		ListCell   *fc_l;

		foreach(fc_l, fc_rel->baserestrictinfo)
		{
			RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_l);
			Node	   *fc_clause = (Node *) fc_rinfo->clause;

			if (fc_rinfo->pseudoconstant)
			{
				fc_upperrestrictlist = lappend(fc_upperrestrictlist, fc_rinfo);
				continue;
			}

			switch (fc_qual_is_pushdown_safe(fc_subquery, fc_rti, fc_rinfo, &fc_safetyInfo))
			{
				case PUSHDOWN_SAFE:
					/* 下推它 */
					fc_subquery_push_qual(fc_subquery, fc_rte, fc_rti, fc_clause);
					break;

				case PUSHDOWN_WINDOWCLAUSE_RUNCOND:

					/*
					 * 由于我们无法将条件下推到子查询中，
					 * 检查它是否引用了窗口函数。 如果是，
					 * 那么这可能对 WindowAgg 的
					 * runCondition 有用。
					 */
					if (!fc_subquery->hasWindowFuncs ||
						fc_check_and_push_window_quals(fc_subquery, fc_rte, fc_rti, fc_clause,
													&fc_run_cond_attrs))
					{
						/*
						 * 子查询没有窗口函数，或者子句不是
						 * 合适的窗口运行条件，或者它是，但
						 * 原始条件也必须保留在上层查询中。
						 */
						fc_upperrestrictlist = lappend(fc_upperrestrictlist, fc_rinfo);
					}
					break;

				case PUSHDOWN_UNSAFE:
					fc_upperrestrictlist = lappend(fc_upperrestrictlist, fc_rinfo);
					break;
			}
		}
		fc_rel->baserestrictinfo = fc_upperrestrictlist;
		/* 我们不在乎重新计算 baserestrict_min_security */
	}

	pfree(fc_safetyInfo.unsafeFlags);

	/*
	 * 上层查询可能不会使用子查询的所有输出列；如果
	 * 不这样做，我们可以简化。 将下推到
	 * WindowAgg 运行条件中的属性传递，以确保我们不会意外认为这些
	 * 是未使用的。
	 */
	fc_remove_unused_subquery_outputs(fc_subquery, fc_rel, fc_run_cond_attrs);

	/*
	 * 如果外层没有连接、聚合或排序需要处理，
	 * 我们可以安全地将 outer tuple_fraction 传递给子查询。
	 * 否则，我们最好告知子查询进行完全检索的规划。 （XXX 这
	 * 可能会更智能...）
	 */
	if (fc_parse->hasAggs ||
		fc_parse->groupClause ||
		fc_parse->groupingSets ||
		fc_parse->havingQual ||
		fc_parse->distinctClause ||
		fc_parse->sortClause ||
		fc_has_multiple_baserels(fc_root))
		fc_tuple_fraction = 0.0;	/* 默认情况 */
	else
		fc_tuple_fraction = fc_root->tuple_fraction;

	/* plan_params 不应该在当前查询级别中使用 */
	Assert(fc_root->plan_params == NIL);

	/* 为子查询生成 subroot 和路径 */
	fc_rel->subroot = subquery_planner(fc_root->glob, fc_subquery,
									fc_root,
									false, fc_tuple_fraction);

	/* 隔离此特定子计划所需的参数 */
	fc_rel->subplan_params = fc_root->plan_params;
	fc_root->plan_params = NIL;

	/*
	 * 可能是约束排除使得子查询为空。 如果是，
	 * 生成一个未装饰的虚拟路径是可取的，以便我们在此查询级别
	 * 识别出合适的优化。
	 */
	fc_sub_final_rel = fetch_upper_rel(fc_rel->subroot, UPPERREL_FINAL, NULL);

	if (IS_DUMMY_REL(fc_sub_final_rel))
	{
		set_dummy_rel_pathlist(fc_rel);
		return;
	}

	/*
	 * 用估算的输出行数、宽度等标记关系。 请注意，
	 * 我们必须在生成外部查询路径之前执行此操作，否则
	 * cost_subqueryscan 不会高兴。
	 */
	set_subquery_size_estimates(fc_root, fc_rel);

	/*
	 * 对于子查询规划器生成的每个路径，在外部查询中创建一个 SubqueryScanPath。
	 */
	foreach(fc_lc, fc_sub_final_rel->pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);
		List	   *fc_pathkeys;

		/* 将子路径的路径键转换为外部表示 */
		fc_pathkeys = convert_subquery_pathkeys(fc_root,
											 fc_rel,
											 fc_subpath->pathkeys,
											 make_tlist_from_pathtarget(fc_subpath->pathtarget));

		/* 使用此子路径生成外部路径 */
		add_path(fc_rel, (Path *)
				 create_subqueryscan_path(fc_root, fc_rel, fc_subpath,
										  fc_pathkeys, fc_required_outer));
	}

	/* 如果外部关系允许并行处理，则对部分路径执行相同操作。 */
	if (fc_rel->consider_parallel && bms_is_empty(fc_required_outer))
	{
		/* 如果 consider_parallel 为 false，则不应该有部分路径。 */
		Assert(fc_sub_final_rel->consider_parallel ||
			   fc_sub_final_rel->partial_pathlist == NIL);

		/* 部分路径也是如此。 */
		foreach(fc_lc, fc_sub_final_rel->partial_pathlist)
		{
			Path	   *fc_subpath = (Path *) lfirst(fc_lc);
			List	   *fc_pathkeys;

			/* 将子路径的路径键转换为外部表示 */
			fc_pathkeys = convert_subquery_pathkeys(fc_root,
												 fc_rel,
												 fc_subpath->pathkeys,
												 make_tlist_from_pathtarget(fc_subpath->pathtarget));

			/* 使用此子路径生成外部路径 */
			add_partial_path(fc_rel, (Path *)
							 create_subqueryscan_path(fc_root, fc_rel, fc_subpath,
													  fc_pathkeys,
													  fc_required_outer));
		}
	}
}

/*
 * set_function_pathlist
 *		为函数 RTE 构建（单一）访问路径
 */
static void set_function_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;
	List	   *fc_pathkeys = NIL;

	/*
	 * 我们不支持将连接子句下推到函数扫描的条件中，
	 * 但由于函数表达式中的 LATERAL 引用，仍可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/*
	 * 结果被视为无序，除非使用了 ORDINALITY，在这种情况下，
	 * 它按序号列（最后一列）排序。 通过检查在等价类中
	 * 是否使用了那个 Var 来看看我们是否在乎。
	 */
	if (fc_rte->funcordinality)
	{
		AttrNumber	fc_ordattno = fc_rel->max_attr;
		Var		   *fc_var = NULL;
		ListCell   *fc_lc;

		/*
		 * 在 rel 的 targetlist 中是否有一个 Var？
		 * 如果没有，则查询没有引用次序列，或者至少没有
		 * 任何有趣的排序方式。
		 */
		foreach(fc_lc, fc_rel->reltarget->exprs)
		{
			Var		   *fc_node = (Var *) lfirst(fc_lc);

			/* 检查 varno/varlevelsup 只是出于过度谨慎 */
			if (IsA(fc_node, Var) &&
				fc_node->varattno == fc_ordattno &&
				fc_node->varno == fc_rel->relid &&
				fc_node->varlevelsup == 0)
			{
				fc_var = fc_node;
				break;
			}
		}

		/*
		 * 尝试为这个 Var 构建带有 int8 排序的路径键。 我们告知
		 * build_expression_pathkey 不要构建任何新的等价类；如果
		 * Var 尚未在某些 EC 中提及，则意味着没有人
		 * 在意排序。
		 */
		if (fc_var)
			fc_pathkeys = build_expression_pathkey(fc_root,
												(Expr *) fc_var,
												NULL,	/* 在外部连接下方 */
												Int8LessOperator,
												fc_rel->relids,
												false);
	}

	/* 生成适当的路径 */
	add_path(fc_rel, create_functionscan_path(fc_root, fc_rel,
										   fc_pathkeys, fc_required_outer));
}

/*
 * set_values_pathlist
 *		为 VALUES RTE 构建（单一）访问路径
 */
static void set_values_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;

	/*
	 * 我们不支持将连接子句下推到值扫描的条件中，
	 * 但由于值表达式中的 LATERAL 引用，仍可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_valuesscan_path(fc_root, fc_rel, fc_required_outer));
}

/*
 * set_tablefunc_pathlist
 *		为表函数 RTE 构建（单一）访问路径
 */
static void set_tablefunc_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;

	/*
	 * 我们不支持将连接子句推入表函数扫描的资格，但由于函数表达式中的 LATERAL 引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_tablefuncscan_path(fc_root, fc_rel,
											fc_required_outer));
}

/*
 * set_cte_pathlist
 *		为非自引用的 CTE RTE 构建（单一）访问路径
 *
 * 不需要单独的 set_cte_size 阶段，因为我们不支持 CTE 的连接资格参数化路径。
 */
static void set_cte_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Plan	   *fc_cteplan;
	PlannerInfo *fc_cteroot;
	Index		fc_levelsup;
	int			fc_ndx;
	ListCell   *fc_lc;
	int			fc_plan_id;
	Relids		fc_required_outer;

	/*
	 * 找到引用的 CTE，并定位之前为其制定的计划。
	 */
	fc_levelsup = fc_rte->ctelevelsup;
	fc_cteroot = fc_root;
	while (fc_levelsup-- > 0)
	{
		fc_cteroot = fc_cteroot->parent_root;
		if (!fc_cteroot)			/* 不应该发生 */
			elog(ERROR, "bad levelsup for CTE \"%s\"", fc_rte->ctename);
	}

	/*
	 * 注意：如果我们仍在规划 CTE（即，这是来自另一个 CTE 的侧引用），cte_plan_ids 可能会比 cteList 短。
	 * 所以我们不能在这里使用 forboth。
	 */
	fc_ndx = 0;
	foreach(fc_lc, fc_cteroot->parse->cteList)
	{
		CommonTableExpr *fc_cte = (CommonTableExpr *) lfirst(fc_lc);

		if (strcmp(fc_cte->ctename, fc_rte->ctename) == 0)
			break;
		fc_ndx++;
	}
	if (fc_lc == NULL)				/* 不应该发生 */
		elog(ERROR, "could not find CTE \"%s\"", fc_rte->ctename);
	if (fc_ndx >= list_length(fc_cteroot->cte_plan_ids))
		elog(ERROR, "could not find plan for CTE \"%s\"", fc_rte->ctename);
	fc_plan_id = list_nth_int(fc_cteroot->cte_plan_ids, fc_ndx);
	if (fc_plan_id <= 0)
		elog(ERROR, "no plan was made for CTE \"%s\"", fc_rte->ctename);
	fc_cteplan = (Plan *) list_nth(fc_root->glob->subplans, fc_plan_id - 1);

	/* 标记关系的估计输出行数、宽度等 */
	set_cte_size_estimates(fc_root, fc_rel, fc_cteplan->plan_rows);

	/*
	 * 我们不支持将连接子句推入 CTE 扫描的资格，但由于其 tlist 中的 LATERAL 引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_ctescan_path(fc_root, fc_rel, fc_required_outer));
}

/*
 * set_namedtuplestore_pathlist
 *		为命名元组存储 RTE 构建（单一）访问路径
 *
 * 不需要单独的 set_namedtuplestore_size 阶段，因为我们不支持元组存储的连接资格参数化路径。
 */
static void set_namedtuplestore_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							 RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;

	/* 标记关系的估计输出行数、宽度等 */
	set_namedtuplestore_size_estimates(fc_root, fc_rel);

	/*
	 * 我们不支持将连接子句推入元组存储扫描的资格，但由于其 tlist 中的 LATERAL 引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_namedtuplestorescan_path(fc_root, fc_rel, fc_required_outer));

	/* 选择最便宜的路径（在这种情况下相当简单...） */
	set_cheapest(fc_rel);
}

/*
 * set_result_pathlist
 *		为 RTE_RESULT RTE 构建（单一）访问路径
 *
 * 不需要单独的 set_result_size 阶段，因为我们不支持这些 RTE 的连接资格参数化路径。
 */
static void set_result_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel,
					RangeTblEntry *fc_rte)
{
	Relids		fc_required_outer;

	/* 标记关系的估计输出行数、宽度等 */
	set_result_size_estimates(fc_root, fc_rel);

	/*
	 * 我们不支持将连接子句推入 Result 扫描的资格，但由于其 tlist 中的 LATERAL 引用，它仍然可能需要参数化。
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_resultscan_path(fc_root, fc_rel, fc_required_outer));

	/* 选择最便宜的路径（在这种情况下相当简单...） */
	set_cheapest(fc_rel);
}

/*
 * set_worktable_pathlist
 *		为自引用 CTE RTE 构建（单一）访问路径
 *
 * 不需要单独的 set_worktable_size 阶段，因为我们不支持 CTE 的连接资格参数化路径。
 */
static void set_worktable_pathlist(PlannerInfo *fc_root, RelOptInfo *fc_rel, RangeTblEntry *fc_rte)
{
	Path	   *fc_ctepath;
	PlannerInfo *fc_cteroot;
	Index		fc_levelsup;
	Relids		fc_required_outer;

	/*
	 * 我们需要找到非递归项的路径，该路径位于处理递归 UNION 的计划层中，该计划层位于 CTE 来源的 *下方* 一层。
	 */
	fc_levelsup = fc_rte->ctelevelsup;
	if (fc_levelsup == 0)			/* 不应该发生 */
		elog(ERROR, "bad levelsup for CTE \"%s\"", fc_rte->ctename);
	fc_levelsup--;
	fc_cteroot = fc_root;
	while (fc_levelsup-- > 0)
	{
		fc_cteroot = fc_cteroot->parent_root;
		if (!fc_cteroot)			/* 不应该发生 */
			elog(ERROR, "bad levelsup for CTE \"%s\"", fc_rte->ctename);
	}
	fc_ctepath = fc_cteroot->non_recursive_path;
	if (!fc_ctepath)				/* 不应该发生 */
		elog(ERROR, "could not find path for CTE \"%s\"", fc_rte->ctename);

	/* 标记关系的估计输出行数、宽度等 */
	set_cte_size_estimates(fc_root, fc_rel, fc_ctepath->rows);

	/*
	 * 我们不支持将连接子句推入工作表扫描的资格，但由于其 tlist 中的 LATERAL 引用，它仍然可能需要参数化。 
	 * （我不确定考虑到递归引用的限制，这实际上是否可能，但支持起来很简单。）
	 */
	fc_required_outer = fc_rel->lateral_relids;

	/* 生成适当的路径 */
	add_path(fc_rel, create_worktablescan_path(fc_root, fc_rel, fc_required_outer));
}

/*
 * generate_gather_paths
 *		通过在部分路径上推送 Gather 或 Gather Merge 来生成关系的并行访问路径。
 *
 * 这必须在为指定关系创建所有部分路径后调用。 
	 *（否则，add_partial_path 可能会删除某些 GatherPath 或 GatherMergePath 有引用的路径。）
 *
 * 如果我们正在为扫描或连接关系生成路径，override_rows 将为 false， 
 * 我们将仅使用关系的大小估计。但是，当我们为部分分组路径调用时，我们需要覆盖行数估计。
 * （目前还不清楚我们在这里使用的特定值是否最佳，但基础关系没有估计，因此我们必须做点什么。）
 */
void generate_gather_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel, bool fc_override_rows)
{
	Path	   *fc_cheapest_partial_path;
	Path	   *fc_simple_gather_path;
	ListCell   *fc_lc;
	double		fc_rows;
	double	   *fc_rowsp = NULL;

	/* 如果没有部分路径，这里就没有要做的。 */
	if (fc_rel->partial_pathlist == NIL)
		return;

	/* 我们是否应该覆盖关系的行数估计？ */
	if (fc_override_rows)
		fc_rowsp = &fc_rows;

	/*
	 * Gather 的输出总是无序的，所以只有一个感兴趣的部分路径：最便宜的那个。
	 * 由于 add_partial_path 的工作方式，它将是 partial_pathlist 前面的那个。
	 */
	fc_cheapest_partial_path = linitial(fc_rel->partial_pathlist);
	fc_rows =
		fc_cheapest_partial_path->rows * fc_cheapest_partial_path->parallel_workers;
	fc_simple_gather_path = (Path *)
		create_gather_path(fc_root, fc_rel, fc_cheapest_partial_path, fc_rel->reltarget,
						   NULL, fc_rowsp);
	add_path(fc_rel, fc_simple_gather_path);

	/*
	 * 对于每个有用的顺序，我们可以考虑一个保持顺序的 Gather Merge。
	 */
	foreach(fc_lc, fc_rel->partial_pathlist)
	{
		Path	   *fc_subpath = (Path *) lfirst(fc_lc);
		GatherMergePath *fc_path;

		if (fc_subpath->pathkeys == NIL)
			continue;

		fc_rows = fc_subpath->rows * fc_subpath->parallel_workers;
		fc_path = create_gather_merge_path(fc_root, fc_rel, fc_subpath, fc_rel->reltarget,
										fc_subpath->pathkeys, NULL, fc_rowsp);
		add_path(fc_rel, &fc_path->path);
	}
}

/*
 * get_useful_pathkeys_for_relation
 *		确定哪个关系的排序可能是有用的。
 *
 * 按排序顺序获取数据可能是有用的，因为请求的顺序与我们正在规划的整体查询的最终输出排序匹配， 
 * 或因为这可以实现有效的合并连接。在这里，我们尝试弄清楚要考虑哪些 pathkeys。
 *
 * 这允许我们在 gather merge 节点下的索引扫描上进行增量排序，即并行化。
 *
 * 如果 require_parallel_safe 为 true，我们还要求表达式是并行安全的（这允许将排序推到 Gather Merge 之下）。
 *
 * XXX 目前这只能返回具有单个元素的列表，因为它仅查看 query_pathkeys。
 * 因此，我们可能会直接返回 pathkeys，但似乎合理的是，我们将在未来考虑其他排序。例如，
 * 我们可能想考虑对于合并连接有用的 pathkeys。
 */
static List * get_useful_pathkeys_for_relation(PlannerInfo *fc_root, RelOptInfo *fc_rel,
								 bool fc_require_parallel_safe)
{
	List	   *fc_useful_pathkeys_list = NIL;

	
/*
	 * 考虑到 query_pathkeys 总是值得的，因为它可能让我们在
	 * 有部分预排序路径可用时避免总排序，或者将总排序推入
	 * 查询的并行部分。
	 */
	if (fc_root->query_pathkeys)
	{
		ListCell   *fc_lc;
		int			fc_npathkeys = 0;	/* 有用的路径关键字 */

		foreach(fc_lc, fc_root->query_pathkeys)
		{
			PathKey    *fc_pathkey = (PathKey *) lfirst(fc_lc);
			EquivalenceClass *fc_pathkey_ec = fc_pathkey->pk_eclass;

			/*
			 * 我们只能为包含一个
			 * 安全的可早期计算的 EC 成员（可从当前
			 * 关系的 reltarget 计算得出）的路径关键字构建排序，
			 * 所以一旦我们发现没有这样的成员的路径关键字，
			 * 就忽略列表的其余部分。
			 *
			 * 返回任何满足这一要求的路径关键字列表的前缀仍然是有价值的，
			 * 因为我们可能能够进行增量排序。
			 *
			 * 如果请求，确保排序表达式也是并行安全的。
			 */
			if (!relation_can_be_sorted_early(fc_root, fc_rel, fc_pathkey_ec,
											  fc_require_parallel_safe))
				break;

			fc_npathkeys++;
		}

		/*
		 * 整个 query_pathkeys 列表匹配，因此直接附加它，以
		 * 允许通过比较列表指针轻松比较路径关键字。如果我们必须截断
		 * 路径关键字，还是需要进行复制。
		 */
		if (fc_npathkeys == list_length(fc_root->query_pathkeys))
			fc_useful_pathkeys_list = lappend(fc_useful_pathkeys_list,
										   fc_root->query_pathkeys);
		else if (fc_npathkeys > 0)
			fc_useful_pathkeys_list = lappend(fc_useful_pathkeys_list,
										   list_truncate(list_copy(fc_root->query_pathkeys),
														 fc_npathkeys));
	}

	return fc_useful_pathkeys_list;
}

/*
 * generate_useful_gather_paths
 *		通过在部分路径上推送 Gather 或
 *		Gather Merge 来生成关系的并行访问路径。
 *
 * 与普通的 generate_gather_paths 不同，这既考虑输入路径的路径关键字
 * （旨在保持顺序），还考虑可能对集成合并节点上方的节点有用的顺序，
 * 并尝试添加一个排序（常规或增量）来提供该顺序。
 */
void generate_useful_gather_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel, bool fc_override_rows)
{
	ListCell   *fc_lc;
	double		fc_rows;
	double	   *fc_rowsp = NULL;
	List	   *fc_useful_pathkeys_list = NIL;
	Path	   *fc_cheapest_partial_path = NULL;

	/* 如果没有部分路径，这里就没有要做的。 */
	if (fc_rel->partial_pathlist == NIL)
		return;

	/* 我们是否应该覆盖关系的行数估计？ */
	if (fc_override_rows)
		fc_rowsp = &fc_rows;

	/* 生成常规的汇聚（合并）路径 */
	generate_gather_paths(fc_root, fc_rel, fc_override_rows);

	/* 考虑有趣的排序的增量排序 */
	fc_useful_pathkeys_list = get_useful_pathkeys_for_relation(fc_root, fc_rel, true);

	/* 用于显式（完整）排序路径 */
	fc_cheapest_partial_path = linitial(fc_rel->partial_pathlist);

	/*
	 * 考虑每个有趣排序的已排序路径。我们生成增量和完全排序。
	 */
	foreach(fc_lc, fc_useful_pathkeys_list)
	{
		List	   *fc_useful_pathkeys = lfirst(fc_lc);
		ListCell   *fc_lc2;
		bool		fc_is_sorted;
		int			fc_presorted_keys;

		foreach(fc_lc2, fc_rel->partial_pathlist)
		{
			Path	   *fc_subpath = (Path *) lfirst(fc_lc2);
			GatherMergePath *fc_path;

			fc_is_sorted = pathkeys_count_contained_in(fc_useful_pathkeys,
													fc_subpath->pathkeys,
													&fc_presorted_keys);

			/*
			 * 我们不需要考虑子路径已经完全排序的情况，
			 * 因为 generate_gather_paths 已经为每个具有路径关键字的子路径创建了
			 * 一个汇聚合并路径。
			 *
			 * 但是，既然子路径已经排序，我们知道
			 * 不需要考虑在其顶部添加排序（完全或增量），
			 * 所以我们可以继续进行。
			 */
			if (fc_is_sorted)
				continue;

			/*
			 * 考虑对每个有用路径键的最低成本部分路径进行常规排序。我们知道路径没有被排序，因为否则我们不会到达这里。
			 *
			 * 这与在generate_gather_paths中创建的gather路径不是冗余的，因为那不会生成有序的输出。在这里，我们添加了一个显式的排序，以匹配有用的排序。
			 */
			if (fc_cheapest_partial_path == fc_subpath)
			{
				Path	   *fc_tmp;

				fc_tmp = (Path *) create_sort_path(fc_root,
												fc_rel,
												fc_subpath,
												fc_useful_pathkeys,
												-1.0);

				fc_rows = fc_tmp->rows * fc_tmp->parallel_workers;

				fc_path = create_gather_merge_path(fc_root, fc_rel,
												fc_tmp,
												fc_rel->reltarget,
												fc_tmp->pathkeys,
												NULL,
												fc_rowsp);

				add_path(fc_rel, &fc_path->path);

				/* 穿透到这里 */
			}

			/*
			 * 考虑增量排序，但仅当子路径在路径键前缀上已经部分排序时。
			 */
			if (enable_incremental_sort && fc_presorted_keys > 0)
			{
				Path	   *fc_tmp;

				/*
				 * 我们应该已经排除了长度为1的路径键，因为那样presorted_keys > 0将意味着is_sorted为true。
				 */
				Assert(list_length(fc_useful_pathkeys) != 1);

				fc_tmp = (Path *) create_incremental_sort_path(fc_root,
															fc_rel,
															fc_subpath,
															fc_useful_pathkeys,
															fc_presorted_keys,
															-1);

				fc_path = create_gather_merge_path(fc_root, fc_rel,
												fc_tmp,
												fc_rel->reltarget,
												fc_tmp->pathkeys,
												NULL,
												fc_rowsp);

				add_path(fc_rel, &fc_path->path);
			}
		}
	}
}

/*
 * make_rel_from_joinlist
 *	  使用“joinlist”构建访问路径，以指导连接路径的搜索。
 *
 * 有关joinlist数据结构的定义，请参阅deconstruct_jointree()的注释。
 */
static RelOptInfo * fc_make_rel_from_joinlist(PlannerInfo *fc_root, List *fc_joinlist)
{
	int			fc_levels_needed;
	List	   *fc_initial_rels;
	ListCell   *fc_jl;

	/*
	 * 计算子joinlist节点的数量。这是我们必须采用的动态规划算法的深度，以考虑连接子节点的所有方法。
	 */
	fc_levels_needed = list_length(fc_joinlist);

	if (fc_levels_needed <= 0)
		return NULL;			/* 没有要做的事？ */

	/*
	 * 构造一个与子joinlist节点对应的rels列表。这可能包含基本rels和根据子joinlists构建的rels。
	 */
	fc_initial_rels = NIL;
	foreach(fc_jl, fc_joinlist)
	{
		Node	   *fc_jlnode = (Node *) lfirst(fc_jl);
		RelOptInfo *fc_thisrel;

		if (IsA(fc_jlnode, RangeTblRef))
		{
			int			fc_varno = ((RangeTblRef *) fc_jlnode)->rtindex;

			fc_thisrel = find_base_rel(fc_root, fc_varno);
		}
		else if (IsA(fc_jlnode, List))
		{
			/* 递归处理子问题 */
			fc_thisrel = fc_make_rel_from_joinlist(fc_root, (List *) fc_jlnode);
		}
		else
		{
			elog(ERROR, "unrecognized joinlist node type: %d",
				 (int) nodeTag(fc_jlnode));
			fc_thisrel = NULL;		/* 保持编译器安静 */
		}

		fc_initial_rels = lappend(fc_initial_rels, fc_thisrel);
	}

	if (fc_levels_needed == 1)
	{
		/*
		 * 单个joinlist节点，因此我们完成了。
		 */
		return (RelOptInfo *) linitial(fc_initial_rels);
	}
	else
	{
		/*
		 * 考虑我们可以以不同顺序连接rels，使用插件、GEQO或常规连接搜索代码。
		 *
		 * 我们将initial_rels列表放入PlannerInfo字段中，因为has_legal_joinclause()需要查看它（很丑 :-()。
		 */
		fc_root->initial_rels = fc_initial_rels;

		if (join_search_hook)
			return (*join_search_hook) (fc_root, fc_levels_needed, fc_initial_rels);
		else if (enable_geqo && fc_levels_needed >= geqo_threshold)
			return geqo(fc_root, fc_levels_needed, fc_initial_rels);
		else
			return standard_join_search(fc_root, fc_levels_needed, fc_initial_rels);
	}
}

/*
 * standard_join_search
 *	  通过不断寻找将组件关系连接为连接关系的方法来查找查询的可能连接路径。
 *
 * 'levels_needed'是所需的迭代次数，即查询中独立jointree项的数量。这大于1。
 *
 * 'initial_rels'是每个独立jointree项的RelOptInfo节点列表。这些是要连接在一起的组件。
 * 注意levels_needed == list_length(initial_rels)。
 *
 * 返回连接所有原始关系的结果的最终连接关系。至少必须为此关系及所有所需的子关系提供一个实现路径。
 *
 * 为了支持通过更改连接搜索算法来修改规划器行为的可加载插件，我们提供一个钩子变量，允许插件替换或补充此功能。任何这样的钩子必须返回与标准代码相同的最终连接关系，但可能附加了不同的实现路径，并且仅需要为这些路径实例化所需的子连接关系。
 *
 * 对插件作者的注意：在standard_join_search()期间调用的函数会修改root->join_rel_list和root->join_rel_hash。如果您想进行多次连接顺序搜索，您可能需要保存并恢复这些数据结构的原始状态。参见geqo_eval()以获取示例。
 */
RelOptInfo * standard_join_search(PlannerInfo *fc_root, int fc_levels_needed, List *fc_initial_rels)
{
	int			fc_lev;
	RelOptInfo *fc_rel;

	/*
	 * 此函数不能在任何一个规划问题中递归调用，因此join_rel_level[]不能已经在使用中。
	 */
	Assert(fc_root->join_rel_level == NULL);

	/*
	 * 我们采用一种简单的“动态编程”算法：首先找到所有将两个jointree项连接起来的方法，然后找到所有将三个项连接起来的方法（从两个项连接和单个项出发），然后是四项连接，依此类推，直到我们考虑了将所有项连接成一个rel的所有方法。
	 *
	 * root->join_rel_level[j]是所有j项rels的列表。最初，我们将root->join_rel_level[1]设置为表示所有单一jointree项关系。
	 */
	fc_root->join_rel_level = (List **) palloc0((fc_levels_needed + 1) * sizeof(List *));

	fc_root->join_rel_level[1] = fc_initial_rels;

	for (fc_lev = 2; fc_lev <= fc_levels_needed; fc_lev++)
	{
		ListCell   *fc_lc;

		/*
		 * 确定此级别上要连接的所有可能的关系对，并为从每个可用的较低级别关系对制作每一个构建路径。
		 */
		join_search_one_level(fc_root, fc_lev);

		/*
		 * 为每个刚处理的joinrel运行generate_partitionwise_join_paths()和generate_useful_gather_paths()。我们无法在之前做到这一点，因为常规和部分路径可以在join_search_one_level的多个时刻被添加到特定的joinrel中。
		 *
		 * 在那之后，我们完成了为joinrel创建路径，因此运行set_cheapest()。
		 */
		foreach(fc_lc, fc_root->join_rel_level[fc_lev])
		{
			fc_rel = (RelOptInfo *) lfirst(fc_lc);

			/* 为分区连接创建路径。 */
			generate_partitionwise_join_paths(fc_root, fc_rel);

			/*
			 * 除了最上面的扫描/连接rel，考虑收集部分路径。一旦我们知道最终的目标列表，我们将对最上面的扫描/连接rel做同样的事（参见grouping_planner）。
			 */
			if (!bms_equal(fc_rel->relids, fc_root->all_baserels))
				generate_useful_gather_paths(fc_root, fc_rel, false);

			/* 查找并保存此rel的最低成本路径 */
			set_cheapest(fc_rel);

#ifdef OPTIMIZER_DEBUG
			debug_print_rel(fc_root, fc_rel);
#endif
		}
	}

	/*
	 * 我们在最终级别上应该有一个单一的rel。
	 */
	if (fc_root->join_rel_level[fc_levels_needed] == NIL)
		elog(ERROR, "failed to build any %d-way joins", fc_levels_needed);
	Assert(list_length(fc_root->join_rel_level[fc_levels_needed]) == 1);

	fc_rel = (RelOptInfo *) linitial(fc_root->join_rel_level[fc_levels_needed]);

	fc_root->join_rel_level = NULL;

	return fc_rel;
}


/*****************************************************************************
 *			将条件下推到子查询中
 *****************************************************************************/

/*
 * subquery_is_pushdown_safe - 子查询是否安全下推条件?
 *
 * subquery 是正在检查的特定组件查询。topquery 是一组操作树的顶部组件（如果没有涉及集合操作，则为相同的查询）。
 *
 * 这里检查的条件：
 *
 * 1. 如果子查询具有 LIMIT 子句，我们不能下推任何条件，
 * 因为这可能会改变返回的行集。
 *
 * 2. 如果子查询包含 EXCEPT 或 EXCEPT ALL 集合操作，我们不能将
 * 条件下推到其中，因为这可能会改变结果。
 *
 * 3. 如果子查询使用 DISTINCT，我们不能将易变条件下推到其中。
 * 这是因为上层条件在语义上应该只在每个唯一行上评估一次，而不是在每个原始行上评估一次，
 * 如果条件是易变的，则额外的评估可能会改变结果。（这个问题不适用于其他聚合形式，比如 GROUP BY，
 * 因为当这些存在时，我们将其下推到 HAVING 而不是 WHERE，这样条件在聚合后仍然适用。）
 *
 * 4. 如果子查询包含窗口函数，我们不能将易变条件下推到其中。
 * 这里的问题与 DISTINCT 有点不同：一个易变条件可能会在窗口分区的某些行上成功,
 * 而在其他行上失败，从而改变分区内容，因此改变保持的窗口函数的结果。
 *
 * 5. 如果子查询的目标列表中包含任何集合返回函数，我们不能将
 * 易变条件下推到其中。这将把它们下推到 SRF 之下，从而改变它们的评估次数。
 * 此外，一个易变条件可能对某些 SRF 输出行成功而对其他行失败，
 * 如果在 SRF 扩展之前评估这种情况是不可能发生的。
 *
 * 6. 如果子查询具有非空分组集，我们不能下推任何条件。
 * 这里的担忧是，参考“常量”分组列的条件可能会被常量折叠，
 * 这是不适当的，因为该值可能由于分组集扩展而可为 NULL。
 * 如果我们有一种表示形式，可以显示这样的分组列并不是真正的常量，
 * 那么这个限制可以被移除。（还有其他想法可以用来放宽这一限制，
 * 但这是最有可能在未来采取的方法。请注意，只要 subquery_planner
 * 不能将 HAVING 子句移动到 WHERE 中，这一切都没有太大的好处。）
 *
 * 此外，我们对子查询的输出列进行多项检查，以查看
 * 在下推条件中引用它们是否安全。如果发现输出列 k
 * 在引用时不安全，我们在 safetyInfo->unsafeFlags[k] 中设置该原因，
 * 但我们不会整体拒绝子查询，因为列 k 可能不会被某些/所有条件引用。
 * unsafeFlags[] 数组将在后面由 qual_is_pushdown_safe() 查询。
 * 这样做要比直接在 qual_is_pushdown_safe() 中进行检查更好，
 * 因为当子查询涉及集合操作时，我们必须检查集合操作的每一支路中的输出表达式。
 *
 * 注意：将条件下推到 DISTINCT 子查询在理论上是可疑的：
 * 我们实际上假设这些条件不能区分 DISTINCT 的等于操作符视为相等的值，
 * 然而对此假设有许多反例。然而，使用这样的条件与 DISTINCT 子查询一起
 * 使用无论如何都是不安全的，因为没有保证“相等”的值会被 DISTINCT 操作
 * 选择作为输出值。所以我们并不太担心这一点。另一个反对意见是，如果该条件
 * 评估成本很高，则对每个原始行评估它可能花费的成本超过在 DISTINCT 步骤之前消除行所节省的成本。
 * 但在这个阶段很难估计这一点，而且在实践中，条件下推似乎很少使事情变得更糟，
 * 所以我们也忽略了这个问题。
 *
 * 注意：同样，将条件下推到有窗口函数的子查询也有点可疑：
 * 这些条件可能会删除窗口分区中的某些行而保留其他行，从而导致生存行的
 * 窗口函数结果发生变化。我们坚持认为这样的条件引用只能是分区列，
 * 但这再次只在条件不能区分分区等于操作符视为相等的值时保护我们。
 * 在此，风险可能比 DISTINCT 更大，因为不发生去重，因此没有理论问题。
 * 但是我们仍然会这样做，因为潜在的性能收益非常大，
 * 而且我们没有看到有关长期以来与 DISTINCT 相比行为的投诉。
 */
static bool fc_subquery_is_pushdown_safe(Query *fc_subquery, Query *fc_topquery,
						  pushdown_safety_info *fc_safetyInfo)
{
	SetOperationStmt *fc_topop;

	/* 检查点 1 */
	if (fc_subquery->limitOffset != NULL || fc_subquery->limitCount != NULL)
		return false;

	
/* 检查点 6 */
	if (fc_subquery->groupClause && fc_subquery->groupingSets)
		return false;

	/* 检查点 3、4 和 5 */
	if (fc_subquery->distinctClause ||
		fc_subquery->hasWindowFuncs ||
		fc_subquery->hasTargetSRFs)
		fc_safetyInfo->unsafeVolatile = true;

	/*
	 * 如果我们处于叶子查询，检查其目标列表中的不安全表达式，
	 * 并在 unsafeFlags[] 中标记它们不安全的任何原因。
	 * （集合操作树中的非叶节点仅在其 tlists 中包含简单的 Vars，
	 * 所以无需检查它们。）
	 */
	if (fc_subquery->setOperations == NULL)
		fc_check_output_expressions(fc_subquery, fc_safetyInfo);

	/* 我们是在顶层，还是在查看集合操作组件？ */
	if (fc_subquery == fc_topquery)
	{
		/* 顶层，因此检查任何组件查询 */
		if (fc_subquery->setOperations != NULL)
			if (!fc_recurse_pushdown_safe(fc_subquery->setOperations, fc_topquery,
									   fc_safetyInfo))
				return false;
	}
	else
	{
		/* 集合操作组件不能有更多组件（太奇怪） */
		if (fc_subquery->setOperations != NULL)
			return false;
		/* 检查集合操作组件的输出类型是否与顶层匹配 */
		fc_topop = castNode(SetOperationStmt, fc_topquery->setOperations);
		Assert(fc_topop);
		fc_compare_tlist_datatypes(fc_subquery->targetList,
								fc_topop->colTypes,
								fc_safetyInfo);
	}
	return true;
}

/*
 * 辅助例程，通过 setOperations 树递归
 */
static bool fc_recurse_pushdown_safe(Node *fc_setOp, Query *fc_topquery,
					  pushdown_safety_info *fc_safetyInfo)
{
	if (IsA(fc_setOp, RangeTblRef))
	{
		RangeTblRef *fc_rtr = (RangeTblRef *) fc_setOp;
		RangeTblEntry *fc_rte = rt_fetch(fc_rtr->rtindex, fc_topquery->rtable);
		Query	   *fc_subquery = fc_rte->subquery;

		Assert(fc_subquery != NULL);
		return fc_subquery_is_pushdown_safe(fc_subquery, fc_topquery, fc_safetyInfo);
	}
	else if (IsA(fc_setOp, SetOperationStmt))
	{
		SetOperationStmt *fc_op = (SetOperationStmt *) fc_setOp;

		/* EXCEPT 不适用（子查询下推安全性点2） */
		if (fc_op->op == SETOP_EXCEPT)
			return false;
		/* 否则递归 */
		if (!fc_recurse_pushdown_safe(fc_op->larg, fc_topquery, fc_safetyInfo))
			return false;
		if (!fc_recurse_pushdown_safe(fc_op->rarg, fc_topquery, fc_safetyInfo))
			return false;
	}
	else
	{
		elog(ERROR, "unrecognized node type: %d",
			 (int) nodeTag(fc_setOp));
	}
	return true;
}

/*
 * check_output_expressions - 检查子查询的输出表达式是否安全
 *
 * 在几种情况下，如果上层的 qual 引用子查询的特定输出列，则下推
 * 是不安全的。我们检查子查询的每个输出列，并在看到某列
 * 不适合下推的 qual 引用时在 unsafeFlags[k] 中设置标志。
 * 这里检查的条件有：
 *
 * 1. 我们不得下推任何引用返回集合的子选择输出的 qual，否则我们
 * 将在子查询的 WHERE/HAVING qual 中引入返回集合的函数。
 *
 * 2. 我们不得下推任何引用包含易变函数的子选择输出的 qual，
 * 以免因多次评估一个易变函数而引入奇怪的结果。
 *
 * 3. 如果子查询使用 DISTINCT ON，我们不得下推任何引用非 DISTINCT
 * 输出列的 qual，因为这可能会改变返回的行集。 （这个条件对 DISTINCT 
 * 是无效的，因为那时没有非 DISTINCT 输出列，因此我们不需要检查。
 * 注意，subquery_is_pushdown_safe 已经报告了如果有 DISTINCT 或 DISTINCT ON
 * 我们不能使用易变的 qual。）
 *
 * 4. 如果子查询有任何窗口函数，我们不得下推引用不在所有子查询窗口
 * PARTITION BY 子句中的输出列的 qual。我们可以下推仅使用分区列的
 * qual，因为对于任何一个窗口分区的每一行，它们应该成功或失败
 * 的结果是相同的，并且完全排除某些分区不会改变其他分区的窗口
 * 函数的结果。 （同样，这也要求是非易变的 qual，但 subquery_is_pushdown_safe
 * 会处理这个问题。）由于这个原因被标记为不安全的子查询列仍然可以
 * 执行 WindowClause 运行条件的下推。
 */
static void fc_check_output_expressions(Query *fc_subquery, pushdown_safety_info *fc_safetyInfo)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_subquery->targetList)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_lc);

		if (fc_tle->resjunk)
			continue;			/* 忽略 resjunk 列 */

		/* 返回集合的函数是不安全的（点1） */
		if (fc_subquery->hasTargetSRFs &&
			(fc_safetyInfo->unsafeFlags[fc_tle->resno] &
			 UNSAFE_HAS_SET_FUNC) == 0 &&
			expression_returns_set((Node *) fc_tle->expr))
		{
			fc_safetyInfo->unsafeFlags[fc_tle->resno] |= UNSAFE_HAS_SET_FUNC;
			continue;
		}

		/* 易变函数是不安全的（点2） */
		if ((fc_safetyInfo->unsafeFlags[fc_tle->resno] &
			 UNSAFE_HAS_VOLATILE_FUNC) == 0 &&
			contain_volatile_functions((Node *) fc_tle->expr))
		{
			fc_safetyInfo->unsafeFlags[fc_tle->resno] |= UNSAFE_HAS_VOLATILE_FUNC;
			continue;
		}

		/* 如果子查询使用 DISTINCT ON，检查点3 */
		if (fc_subquery->hasDistinctOn &&
			(fc_safetyInfo->unsafeFlags[fc_tle->resno] &
			 UNSAFE_NOTIN_DISTINCTON_CLAUSE) == 0 &&
			!targetIsInSortList(fc_tle, InvalidOid, fc_subquery->distinctClause))
		{
			/* non-DISTINCT 列，因此标记为不安全 */
			fc_safetyInfo->unsafeFlags[fc_tle->resno] |= UNSAFE_NOTIN_DISTINCTON_CLAUSE;
			continue;
		}

		/* 如果子查询使用窗口函数，检查点4 */
		if (fc_subquery->hasWindowFuncs &&
			(fc_safetyInfo->unsafeFlags[fc_tle->resno] &
			 UNSAFE_NOTIN_DISTINCTON_CLAUSE) == 0 &&
			!fc_targetIsInAllPartitionLists(fc_tle, fc_subquery))
		{
			/* 不存在于所有 PARTITION BY 子句中，因此标记为不安全 */
			fc_safetyInfo->unsafeFlags[fc_tle->resno] |= UNSAFE_NOTIN_PARTITIONBY_CLAUSE;
			continue;
		}
	}
}

/*
 * 对于使用 UNION/UNION ALL/INTERSECT/INTERSECT ALL 的子查询，
 * 我们可以将 quals 下推到每个组成查询中，但这些 quals 只能引用在
 * 集合操作中没有发生类型强制转换的子查询列。
 * 否则可能出现语义错误。因此，我们检查组件查询，以查看
 * 其中任何一个的输出类型是否与顶层集合操作输出不同。
 * 如果列 k 在任何组件中类型不同，我们在 unsafeFlags[k] 中
 * 设置 UNSAFE_TYPE_MISMATCH 位。
 *
 * 我们在这里不需要关心 typmods：集合操作输入和输出 typmods
 * 之间唯一允许的差异是输入是特定的 typmod，而输出是 -1，
 * 这并不需要强制转换。
 *
 * tlist 是子查询的 tlist。
 * colTypes 是顶层集合操作的输出列类型的 OID 列表。
 * safetyInfo 是用于设置 unsafeFlags[] 的 pushdown_safety_info。
 */
static void fc_compare_tlist_datatypes(List *fc_tlist, List *fc_colTypes,
						pushdown_safety_info *fc_safetyInfo)
{
	ListCell   *fc_l;
	ListCell   *fc_colType = list_head(fc_colTypes);

	foreach(fc_l, fc_tlist)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_l);

		if (fc_tle->resjunk)
			continue;			/* 忽略 resjunk 列 */
		if (fc_colType == NULL)
			elog(ERROR, "wrong number of tlist entries");
		if (exprType((Node *) fc_tle->expr) != lfirst_oid(fc_colType))
			fc_safetyInfo->unsafeFlags[fc_tle->resno] |= UNSAFE_TYPE_MISMATCH;
		fc_colType = lnext(fc_colTypes, fc_colType);
	}
	if (fc_colType != NULL)
		elog(ERROR, "wrong number of tlist entries");
}

/*
 * targetIsInAllPartitionLists
 *		如果 TargetEntry 在查询中每个窗口定义的 PARTITION BY 子句中都列出，则为真。
 *
 * 忽略实际上未被任何窗口函数使用的窗口是安全的，但在这个阶段
 * 获取该信息并不容易；而且，考虑到未经引用的窗口定义在实践中
 * 可能不常见，因此花额外的周期获取它似乎也不太有用。
 */
static bool fc_targetIsInAllPartitionLists(TargetEntry *fc_tle, Query *fc_query)
{
	ListCell   *fc_lc;

	foreach(fc_lc, fc_query->windowClause)
	{
		WindowClause *fc_wc = (WindowClause *) lfirst(fc_lc);

		if (!targetIsInSortList(fc_tle, InvalidOid, fc_wc->partitionClause))
			return false;
	}
	return true;
}

/*
 * qual_is_pushdown_safe - 特定的 rinfo 是否安全进行下推？
 *
 * rinfo 是适用给定子查询的限制子句（其 RTE 在父查询中的索引为 rti）。
 *
 * 这里检查的条件：
 *
 * 1. rinfo 的子句不得包含任何 SubPlans（主要是因为不清楚这是否
 * 能正确工作：SubLinks 将已经在 qual 中转换为 SubPlans，但
 * 不在子查询中）。注意，转换为 initplans 的 SubLinks 是安全的，
 * 将被接受，因为我们将在 qual 中看到的只是引用 initplan 输出的 Param。
 *
 * 2. 如果 unsafeVolatile 被设置，rinfo 的子句不得包含任何易变函数。
 *
 * 3. 如果 unsafeLeaky 被设置，rinfo 的子句不得包含任何传递 Var 节点的
 * 漏泄函数，因此可能会作为副作用披露子查询的值。
 *
 * 4. rinfo 的子句不得引用子查询的整行输出（因为没有简单的方法
 * 可以在子查询中命名它）。
 *
 * 5. rinfo 的子句不得引用任何被 subquery_is_pushdown_safe()
 * 发现不安全的子查询输出列。
 */
static pushdown_safe_type
fc_qual_is_pushdown_safe(Query *fc_subquery, Index fc_rti, RestrictInfo *fc_rinfo,
					  pushdown_safety_info *fc_safetyInfo)
{
	pushdown_safe_type fc_safe = PUSHDOWN_SAFE;
	Node	   *fc_qual = (Node *) fc_rinfo->clause;
	List	   *fc_vars;
	ListCell   *fc_vl;

	
/* 拒绝子查询（第1点） */
	if (contain_subplans(fc_qual))
		return PUSHDOWN_UNSAFE;

	/* 如果发现不安全则拒绝易变的条件（第2点） */
	if (fc_safetyInfo->unsafeVolatile &&
		contain_volatile_functions((Node *) fc_rinfo))
		return PUSHDOWN_UNSAFE;

	/* 如果被告知则拒绝泄漏的条件（第3点） */
	if (fc_safetyInfo->unsafeLeaky &&
		contain_leaked_vars(fc_qual))
		return PUSHDOWN_UNSAFE;

	/* 
	 * 推送窗口函数调用是不安全的，但至少目前我们在条件中
	 * 根本看不到任何窗口函数。（同样适用于聚合，我们在下面的
	 * pull_var_clause 中检查。）
	 */
	Assert(!contain_window_function(fc_qual));

	/* 
	 * 检查在子句中使用的所有变量。由于这是一个限制条件，
	 * 所有这样的变量必须引用子查询输出列……除非这是
	 * LATERAL 子查询的一部分，在这种情况下可能存在横向
	 * 引用。
	 */
	fc_vars = pull_var_clause(fc_qual, PVC_INCLUDE_PLACEHOLDERS);
	foreach(fc_vl, fc_vars)
	{
		Var		   *fc_var = (Var *) lfirst(fc_vl);

		/* 
		 * 如果在限制条件中发现任何 PlaceHolderVars，请跳过。
		 * 不清楚 PHV 是否可以安全地向下推送，甚至更不清楚
		 * 这种情况是否在任何实际相关的情况下发生。因此，
		 * 目前只拒绝向下推送。
		 */
		if (!IsA(fc_var, Var))
		{
			fc_safe = PUSHDOWN_UNSAFE;
			break;
		}

		/* 
		 * 如果我们发现任何横向引用，请跳过。推送这些是安全的，
		 * 但我们必须将其转换为外部引用，而 subquery_push_qual
		 * 缺乏这样做的基础设施。这种情况发生得很少，
		 * 所以似乎没有必要费心去研究。
		 */
		if (fc_var->varno != fc_rti)
		{
			fc_safe = PUSHDOWN_UNSAFE;
			break;
		}

		/* 子查询没有系统列 */
		Assert(fc_var->varattno >= 0);

		/* 检查点 4 */
		if (fc_var->varattno == 0)
		{
			fc_safe = PUSHDOWN_UNSAFE;
			break;
		}

		/* 检查点 5 */
		if (fc_safetyInfo->unsafeFlags[fc_var->varattno] != 0)
		{
			if (fc_safetyInfo->unsafeFlags[fc_var->varattno] &
				(UNSAFE_HAS_VOLATILE_FUNC | UNSAFE_HAS_SET_FUNC |
				 UNSAFE_NOTIN_DISTINCTON_CLAUSE | UNSAFE_TYPE_MISMATCH))
			{
				fc_safe = PUSHDOWN_UNSAFE;
				break;
			}
			else
			{
				/* UNSAFE_NOTIN_PARTITIONBY_CLAUSE 对运行条件是可以的 */
				fc_safe = PUSHDOWN_WINDOWCLAUSE_RUNCOND;
				/* 不能中断，我们可能会发现另一个不安全的变量 */
			}
		}
	}

	list_free(fc_vars);

	return fc_safe;
}

/* 
 * subquery_push_qual - 向下推送我们已经确定是安全的条件
 */
static void fc_subquery_push_qual(Query *fc_subquery, RangeTblEntry *fc_rte, Index fc_rti, Node *fc_qual)
{
	if (fc_subquery->setOperations != NULL)
	{
		/* 递归地分别将其推送到每个组件查询 */
		fc_recurse_push_qual(fc_subquery->setOperations, fc_subquery,
						  fc_rte, fc_rti, fc_qual);
	}
	else
	{
		/* 
		 * 我们需要在条件中替换变量（必须引用
		 * 子查询的输出）为子查询的目标列表表达式的副本。
		 * 请注意，在这一点上，条件中的任何上层变量都应
		 * 被替换为参数，因此不需要处理。
		 *
		 * 此步骤还确保，在推送到集合树时，每个组件查询
		 * 得到自己的条件副本。
		 */
		fc_qual = ReplaceVarsFromTargetList(fc_qual, fc_rti, 0, fc_rte,
										 fc_subquery->targetList,
										 REPLACEVARS_REPORT_ERROR, 0,
										 &fc_subquery->hasSubLinks);

		/* 
		 * 现在将条件附加到合适的位置：通常是 WHERE，但如果
		 * 子查询使用分组或聚合，则放入 HAVING（因为条件
		 * 实际上是指向组结果行）。
		 */
		if (fc_subquery->hasAggs || fc_subquery->groupClause || fc_subquery->groupingSets || fc_subquery->havingQual)
			fc_subquery->havingQual = make_and_qual(fc_subquery->havingQual, fc_qual);
		else
			fc_subquery->jointree->quals =
				make_and_qual(fc_subquery->jointree->quals, fc_qual);

		/*
		 * 我们不需要改变子查询的 hasAggs 或 hasSubLinks 标志，
		 * 因为我们不能推送任何之前不存在的聚合，同时我们根本不推送子查询。
		 */
	}
}

/*
 * 辅助例程，通过 setOperations 树递归
 */
static void fc_recurse_push_qual(Node *fc_setOp, Query *fc_topquery,
				  RangeTblEntry *fc_rte, Index fc_rti, Node *fc_qual)
{
	if (IsA(fc_setOp, RangeTblRef))
	{
		RangeTblRef *fc_rtr = (RangeTblRef *) fc_setOp;
		RangeTblEntry *fc_subrte = rt_fetch(fc_rtr->rtindex, fc_topquery->rtable);
		Query	   *fc_subquery = fc_subrte->subquery;

		Assert(fc_subquery != NULL);
		fc_subquery_push_qual(fc_subquery, fc_rte, fc_rti, fc_qual);
	}
	else if (IsA(fc_setOp, SetOperationStmt))
	{
		SetOperationStmt *fc_op = (SetOperationStmt *) fc_setOp;

		fc_recurse_push_qual(fc_op->larg, fc_topquery, fc_rte, fc_rti, fc_qual);
		fc_recurse_push_qual(fc_op->rarg, fc_topquery, fc_rte, fc_rti, fc_qual);
	}
	else
	{
		elog(ERROR, "unrecognized node type: %d",
			 (int) nodeTag(fc_setOp));
	}
}

/*****************************************************************************
 *			简化子查询目标列表
 *****************************************************************************/

/*
 * remove_unused_subquery_outputs
 *		删除不需要的子查询目标列表项
 *
 * 上层查询可能并不读取子查询的所有输出列。我们可以删除任何子查询本身不需要的输出（例如，作为排序/分组列）且不影响语义的输出（例如，易变函数不能被删除）。这不仅是因为我们可能能够删除昂贵计算的表达式，还因为删除输出列可能允许在子查询内进行如连接删除等优化。
 *
 * extra_used_attrs 可以作为非空参数传递，以标记我们不应删除的任何列（偏移由 FirstLowInvalidHeapAttributeNumber）。此参数会被函数修改，因此调用者必须在调用该函数后进行复制，以便使用传入的 Bitmapset。
 *
 * 为了避免影响目标列表中的列编号，我们并不实际删除未使用的目标列表项，而是将它们的表达式替换为 NULL 常量。这是通过修改 subquery->targetList 实现的。
 */
static void fc_remove_unused_subquery_outputs(Query *fc_subquery, RelOptInfo *fc_rel,
							   Bitmapset *fc_extra_used_attrs)
{
	Bitmapset  *fc_attrs_used;
	ListCell   *fc_lc;

	/*
	 * 直接指向 extra_used_attrs。不需要 bms_copy，因为当前的调用者在调用该函数后不会使用 Bitmapset。
	 */
	fc_attrs_used = fc_extra_used_attrs;

	/*
	 * 如果子查询有 UNION/INTERSECT/EXCEPT，则不进行任何操作：原则上我们可以更新所有子 SELECT 的目标列表，但现在似乎不值得。
	 */
	if (fc_subquery->setOperations)
		return;

	/*
	 * 如果子查询有常规 DISTINCT（而不是 DISTINCT ON），我们是在浪费时间：它的所有输出列必须在 distinctClause 中使用。
	 */
	if (fc_subquery->distinctClause && !fc_subquery->hasDistinctOn)
		return;

	/*
	 * 收集上层查询使用的所有输出列的编号的位图。
	 *
	 * 添加所有在连接或最终输出中所需的属性。注意：我们必须查看 rel 的目标列表，而不是 attr_needed 数据，因为 attr_needed 不会为继承的子关系计算，见 set_append_rel_size()。 
	 * （XXX 可能有必要在某个时候改变这一点。）
	 */
	pull_varattnos((Node *) fc_rel->reltarget->exprs, fc_rel->relid, &fc_attrs_used);

	/* 添加所有未下推的限制子句使用的属性。 */
	foreach(fc_lc, fc_rel->baserestrictinfo)
	{
		RestrictInfo *fc_rinfo = (RestrictInfo *) lfirst(fc_lc);

		pull_varattnos((Node *) fc_rinfo->clause, fc_rel->relid, &fc_attrs_used);
	}

	/*
	 * 如果有对子查询的整行引用，我们不能删除任何内容。
	 */
	if (bms_is_member(0 - FirstLowInvalidHeapAttributeNumber, fc_attrs_used))
		return;

	/*
	 * 遍历目标列表并删除我们不需要的条目。因为 set_subquery_pathlist 复制了子查询，所以在原地修改目标列表项也是可以的。
	 */
	foreach(fc_lc, fc_subquery->targetList)
	{
		TargetEntry *fc_tle = (TargetEntry *) lfirst(fc_lc);
		Node	   *fc_texpr = (Node *) fc_tle->expr;

		/*
		 * 如果它有一个 sortgroupref 编号，则在某个排序/分组子句中使用，因此我们最好不要删除它。此外，不要删除任何 resjunk 列，因为它们存在的原因与任何人读取子查询的输出无关。（在子 SELECT 中，resjunk 列可能总是设置了 ressortgroupref，但即使没有，删除它们似乎也是不明智的。）
		 */
		if (fc_tle->ressortgroupref || fc_tle->resjunk)
			continue;

		/*
		 * 如果上层查询使用了它，我们不能删除它。
		 */
		if (bms_is_member(fc_tle->resno - FirstLowInvalidHeapAttributeNumber,
						  fc_attrs_used))
			continue;

		/*
		 * 如果它包含一个集返回函数，我们不能删除它，因为这可能改变子查询返回的行数。
		 */
		if (fc_subquery->hasTargetSRFs &&
			expression_returns_set(fc_texpr))
			continue;

		/*
		 * 如果它包含易变函数，我们不敢删除它，以免用户期待其副作用发生。
		 */
		if (contain_volatile_functions(fc_texpr))
			continue;

		/*
		 * 好的，我们不需要它。用 NULL 常量替换表达式。保留表达式的显露类型，以防有东西查看子查询结果的行类型。
		 */
		fc_tle->expr = (Expr *) makeNullConst(exprType(fc_texpr),
										   exprTypmod(fc_texpr),
										   exprCollation(fc_texpr));
	}
}

/*
 * create_partial_bitmap_paths
 *	  为关系构建部分位图堆路径
 */
void create_partial_bitmap_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel,
							Path *fc_bitmapqual)
{
	int			fc_parallel_workers;
	double		fc_pages_fetched;

	/* 计算位图堆扫描的堆页面 */
	fc_pages_fetched = compute_bitmap_pages(fc_root, fc_rel, fc_bitmapqual, 1.0,
										 NULL, NULL);

	fc_parallel_workers = compute_parallel_worker(fc_rel, fc_pages_fetched, -1,
											   max_parallel_workers_per_gather);

	if (fc_parallel_workers <= 0)
		return;

	add_partial_path(fc_rel, (Path *) create_bitmap_heap_path(fc_root, fc_rel,
														   fc_bitmapqual, fc_rel->lateral_relids, 1.0, fc_parallel_workers));
}

/*
 * 计算应使用多少个并行工作者来扫描一个关系。我们根据要扫描的堆的大小和要扫描的索引的大小来计算并行工作者，然后选择两者中的最小值。
 *
 * "heap_pages" 是我们预计从表中扫描的页面数量，或如果不预计扫描则为 -1。
 *
 * "index_pages" 是我们预计从索引中扫描的页面数量，或如果不预计扫描则为 -1。
 *
 * "max_workers" 是调用者对工作者数量的限制。通常来自 GUC。
 */
int compute_parallel_worker(RelOptInfo *fc_rel, double fc_heap_pages, double fc_index_pages,
						int fc_max_workers)
{
	int			fc_parallel_workers = 0;

	/*
	 * 如果用户设置了 parallel_workers 关系选项，使用该值；否则选择默认的工作者数量。
	 */
	if (fc_rel->rel_parallel_workers != -1)
		fc_parallel_workers = fc_rel->rel_parallel_workers;
	else
	{
		/*
		 * 如果被扫描的页面数量不足以证明使用并行扫描，只需返回零……除非它是继承子关系。在这种情况下，我们无论如何都想在这里生成一个并行路径。单独看这个关系可能没有价值，但与所有继承兄弟结合使用时可能会有回报。
		 */
		if (fc_rel->reloptkind == RELOPT_BASEREL &&
			((fc_heap_pages >= 0 && fc_heap_pages < min_parallel_table_scan_size) ||
			 (fc_index_pages >= 0 && fc_index_pages < min_parallel_index_scan_size)))
			return 0;

		if (fc_heap_pages >= 0)
		{
			int			fc_heap_parallel_threshold;
			int			fc_heap_parallel_workers = 1;

			
/*
			 * 根据关系的大小的对数选择工作者数量。 这可能需要更复杂一些，但我们现在需要一些东西。 请注意，min_parallel_table_scan_size GUC 的上限是为了防止这里的溢出而选择的。
			 */
			fc_heap_parallel_threshold = Max(min_parallel_table_scan_size, 1);
			while (fc_heap_pages >= (BlockNumber) (fc_heap_parallel_threshold * 3))
			{
				fc_heap_parallel_workers++;
				fc_heap_parallel_threshold *= 3;
				if (fc_heap_parallel_threshold > INT_MAX / 3)
					break;		/* 避免溢出 */
			}

			fc_parallel_workers = fc_heap_parallel_workers;
		}

		if (fc_index_pages >= 0)
		{
			int			fc_index_parallel_workers = 1;
			int			fc_index_parallel_threshold;

			/* 与上面的 heap_pages 计算相同 */
			fc_index_parallel_threshold = Max(min_parallel_index_scan_size, 1);
			while (fc_index_pages >= (BlockNumber) (fc_index_parallel_threshold * 3))
			{
				fc_index_parallel_workers++;
				fc_index_parallel_threshold *= 3;
				if (fc_index_parallel_threshold > INT_MAX / 3)
					break;		/* 避免溢出 */
			}

			if (fc_parallel_workers > 0)
				fc_parallel_workers = Min(fc_parallel_workers, fc_index_parallel_workers);
			else
				fc_parallel_workers = fc_index_parallel_workers;
		}
	}

	/* 在任何情况下都不要使用超过调用者提供的最大工作者数量 */
	fc_parallel_workers = Min(fc_parallel_workers, fc_max_workers);

	return fc_parallel_workers;
}

/*
 * generate_partitionwise_join_paths
 * 		为给定的分区连接关系创建表示分区连接的路径。
 *
 * 这在我们完成为所有子连接添加路径之前不能被调用。 否则，add_path 可能会删除某些路径，而这些路径在这里生成的某些路径有引用。
 */
void generate_partitionwise_join_paths(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	List	   *fc_live_children = NIL;
	int			fc_cnt_parts;
	int			fc_num_parts;
	RelOptInfo **fc_part_rels;

	/* 这里只处理连接关系。 */
	if (!IS_JOIN_REL(fc_rel))
		return;

	/* 如果关系不是分区的，我们就没有什么可做的。 */
	if (!IS_PARTITIONED_REL(fc_rel))
		return;

	/* 该关系应设置 consider_partitionwise_join。 */
	Assert(fc_rel->consider_partitionwise_join);

	/* 防止由于过深的分区层次结构导致堆栈溢出。 */
	check_stack_depth();

	fc_num_parts = fc_rel->nparts;
	fc_part_rels = fc_rel->part_rels;

	/* 收集非虚拟子连接。 */
	for (fc_cnt_parts = 0; fc_cnt_parts < fc_num_parts; fc_cnt_parts++)
	{
		RelOptInfo *fc_child_rel = fc_part_rels[fc_cnt_parts];

		/* 如果它被完全修剪，那么它一定是虚拟的。 */
		if (fc_child_rel == NULL)
			continue;

		/* 为这个分区子连接创建分区连接路径。 */
		generate_partitionwise_join_paths(fc_root, fc_child_rel);

		/* 如果我们未能为这个子节点创建任何路径，我们必须放弃。 */
		if (fc_child_rel->pathlist == NIL)
		{
			/*
			 * 将父连接关系标记为未分区，以便稍后
			 * 函数能够正确处理。
			 */
			fc_rel->nparts = 0;
			return;
		}

		/* 否则，识别它的最便宜路径。 */
		set_cheapest(fc_child_rel);

		/* 虚拟子节点无需扫描，因此可以忽略。 */
		if (IS_DUMMY_REL(fc_child_rel))
			continue;

#ifdef OPTIMIZER_DEBUG
		debug_print_rel(fc_root, fc_child_rel);
#endif

		fc_live_children = lappend(fc_live_children, fc_child_rel);
	}

	/* 如果所有子连接都是虚拟的，父连接也是虚拟的。 */
	if (!fc_live_children)
	{
		mark_dummy_rel(fc_rel);
		return;
	}

	/* 从子连接路径构建此关系的附加路径。 */
	add_paths_to_append_rel(fc_root, fc_rel, fc_live_children);
	list_free(fc_live_children);
}


/*****************************************************************************
 *			DEBUG 支持
 *****************************************************************************/

#ifdef OPTIMIZER_DEBUG

static void fc_print_relids(PlannerInfo *fc_root, Relids fc_relids)
{
	int			fc_x;
	bool		fc_first = true;

	fc_x = -1;
	while ((fc_x = bms_next_member(fc_relids, fc_x)) >= 0)
	{
		if (!fc_first)
			printf(" ");
		if (fc_x < fc_root->simple_rel_array_size &&
			fc_root->simple_rte_array[fc_x])
			printf("%s", fc_root->simple_rte_array[fc_x]->eref->aliasname);
		else
			printf("%d", fc_x);
		fc_first = false;
	}
}

static void fc_print_restrictclauses(PlannerInfo *fc_root, List *fc_clauses)
{
	ListCell   *fc_l;

	foreach(fc_l, fc_clauses)
	{
		RestrictInfo *fc_c = lfirst(fc_l);

		print_expr((Node *) fc_c->clause, fc_root->parse->rtable);
		if (lnext(fc_clauses, fc_l))
			printf(", ");
	}
}

static void fc_print_path(PlannerInfo *fc_root, Path *fc_path, int fc_indent)
{
	const char *fc_ptype;
	bool		fc_join = false;
	Path	   *fc_subpath = NULL;
	int			fc_i;

	switch (nodeTag(fc_path))
	{
		case T_Path:
			switch (fc_path->pathtype)
			{
				case T_SeqScan:
					fc_ptype = "SeqScan";
					break;
				case T_SampleScan:
					fc_ptype = "SampleScan";
					break;
				case T_FunctionScan:
					fc_ptype = "FunctionScan";
					break;
				case T_TableFuncScan:
					fc_ptype = "TableFuncScan";
					break;
				case T_ValuesScan:
					fc_ptype = "ValuesScan";
					break;
				case T_CteScan:
					fc_ptype = "CteScan";
					break;
				case T_NamedTuplestoreScan:
					fc_ptype = "NamedTuplestoreScan";
					break;
				case T_Result:
					fc_ptype = "Result";
					break;
				case T_WorkTableScan:
					fc_ptype = "WorkTableScan";
					break;
				default:
					fc_ptype = "???Path";
					break;
			}
			break;
		case T_IndexPath:
			fc_ptype = "IdxScan";
			break;
		case T_BitmapHeapPath:
			fc_ptype = "BitmapHeapScan";
			break;
		case T_BitmapAndPath:
			fc_ptype = "BitmapAndPath";
			break;
		case T_BitmapOrPath:
			fc_ptype = "BitmapOrPath";
			break;
		case T_TidPath:
			fc_ptype = "TidScan";
			break;
		case T_TidRangePath:
			fc_ptype = "TidRangePath";
			break;
		case T_SubqueryScanPath:
			fc_ptype = "SubqueryScan";
			break;
		case T_ForeignPath:
			fc_ptype = "ForeignScan";
			break;
		case T_CustomPath:
			fc_ptype = "CustomScan";
			break;
		case T_NestPath:
			fc_ptype = "NestLoop";
			fc_join = true;
			break;
		case T_MergePath:
			fc_ptype = "MergeJoin";
			fc_join = true;
			break;
		case T_HashPath:
			fc_ptype = "HashJoin";
			fc_join = true;
			break;
		case T_AppendPath:
			fc_ptype = "Append";
			break;
		case T_MergeAppendPath:
			fc_ptype = "MergeAppend";
			break;
		case T_GroupResultPath:
			fc_ptype = "GroupResult";
			break;
		case T_MaterialPath:
			fc_ptype = "Material";
			fc_subpath = ((MaterialPath *) fc_path)->subpath;
			break;
		case T_MemoizePath:
			fc_ptype = "Memoize";
			fc_subpath = ((MemoizePath *) fc_path)->subpath;
			break;
		case T_UniquePath:
			fc_ptype = "Unique";
			fc_subpath = ((UniquePath *) fc_path)->subpath;
			break;
		case T_GatherPath:
			fc_ptype = "Gather";
			fc_subpath = ((GatherPath *) fc_path)->subpath;
			break;
		case T_GatherMergePath:
			fc_ptype = "GatherMerge";
			fc_subpath = ((GatherMergePath *) fc_path)->subpath;
			break;
		case T_ProjectionPath:
			fc_ptype = "Projection";
			fc_subpath = ((ProjectionPath *) fc_path)->subpath;
			break;
		case T_ProjectSetPath:
			fc_ptype = "ProjectSet";
			fc_subpath = ((ProjectSetPath *) fc_path)->subpath;
			break;
		case T_SortPath:
			fc_ptype = "Sort";
			fc_subpath = ((SortPath *) fc_path)->subpath;
			break;
		case T_IncrementalSortPath:
			fc_ptype = "IncrementalSort";
			fc_subpath = ((SortPath *) fc_path)->subpath;
			break;
		case T_GroupPath:
			fc_ptype = "Group";
			fc_subpath = ((GroupPath *) fc_path)->subpath;
			break;
		case T_UpperUniquePath:
			fc_ptype = "UpperUnique";
			fc_subpath = ((UpperUniquePath *) fc_path)->subpath;
			break;
		case T_AggPath:
			fc_ptype = "Agg";
			fc_subpath = ((AggPath *) fc_path)->subpath;
			break;
		case T_GroupingSetsPath:
			fc_ptype = "GroupingSets";
			fc_subpath = ((GroupingSetsPath *) fc_path)->subpath;
			break;
		case T_MinMaxAggPath:
			fc_ptype = "MinMaxAgg";
			break;
		case T_WindowAggPath:
			fc_ptype = "WindowAgg";
			fc_subpath = ((WindowAggPath *) fc_path)->subpath;
			break;
		case T_SetOpPath:
			fc_ptype = "SetOp";
			fc_subpath = ((SetOpPath *) fc_path)->subpath;
			break;
		case T_RecursiveUnionPath:
			fc_ptype = "RecursiveUnion";
			break;
		case T_LockRowsPath:
			fc_ptype = "LockRows";
			fc_subpath = ((LockRowsPath *) fc_path)->subpath;
			break;
		case T_ModifyTablePath:
			fc_ptype = "ModifyTable";
			break;
		case T_LimitPath:
			fc_ptype = "Limit";
			fc_subpath = ((LimitPath *) fc_path)->subpath;
			break;
		default:
			fc_ptype = "???Path";
			break;
	}

	for (fc_i = 0; fc_i < fc_indent; fc_i++)
		printf("\t");
	printf("%s", fc_ptype);

	if (fc_path->parent)
	{
		printf("(");
		fc_print_relids(fc_root, fc_path->parent->relids);
		printf(")");
	}
	if (fc_path->param_info)
	{
		printf(" required_outer (");
		fc_print_relids(fc_root, fc_path->param_info->ppi_req_outer);
		printf(")");
	}
	printf(" rows=%.0f cost=%.2f..%.2f\n",
		   fc_path->rows, fc_path->startup_cost, fc_path->total_cost);

	if (fc_path->pathkeys)
	{
		for (fc_i = 0; fc_i < fc_indent; fc_i++)
			printf("\t");
		printf("  pathkeys: ");
		print_pathkeys(fc_path->pathkeys, fc_root->parse->rtable);
	}

	if (fc_join)
	{
		JoinPath   *fc_jp = (JoinPath *) fc_path;

		for (fc_i = 0; fc_i < fc_indent; fc_i++)
			printf("\t");
		printf("  clauses: ");
		fc_print_restrictclauses(fc_root, fc_jp->joinrestrictinfo);
		printf("\n");

		if (IsA(fc_path, MergePath))
		{
			MergePath  *fc_mp = (MergePath *) fc_path;

			for (fc_i = 0; fc_i < fc_indent; fc_i++)
				printf("\t");
			printf("  sortouter=%d sortinner=%d materializeinner=%d\n",
				   ((fc_mp->outersortkeys) ? 1 : 0),
				   ((fc_mp->innersortkeys) ? 1 : 0),
				   ((fc_mp->materialize_inner) ? 1 : 0));
		}

		fc_print_path(fc_root, fc_jp->outerjoinpath, fc_indent + 1);
		fc_print_path(fc_root, fc_jp->innerjoinpath, fc_indent + 1);
	}

	if (fc_subpath)
		fc_print_path(fc_root, fc_subpath, fc_indent + 1);
}

void debug_print_rel(PlannerInfo *fc_root, RelOptInfo *fc_rel)
{
	ListCell   *fc_l;

	printf("RELOPTINFO (");
	fc_print_relids(fc_root, fc_rel->relids);
	printf("): rows=%.0f width=%d\n", fc_rel->rows, fc_rel->reltarget->width);

	if (fc_rel->baserestrictinfo)
	{
		printf("\tbaserestrictinfo: ");
		fc_print_restrictclauses(fc_root, fc_rel->baserestrictinfo);
		printf("\n");
	}

	if (fc_rel->joininfo)
	{
		printf("\tjoininfo: ");
		fc_print_restrictclauses(fc_root, fc_rel->joininfo);
		printf("\n");
	}

	printf("\tpath list:\n");
	foreach(fc_l, fc_rel->pathlist)
		fc_print_path(fc_root, lfirst(fc_l), 1);
	if (fc_rel->cheapest_parameterized_paths)
	{
		printf("\n\tcheapest parameterized paths:\n");
		foreach(fc_l, fc_rel->cheapest_parameterized_paths)
			fc_print_path(fc_root, lfirst(fc_l), 1);
	}
	if (fc_rel->cheapest_startup_path)
	{
		printf("\n\tcheapest startup path:\n");
		fc_print_path(fc_root, fc_rel->cheapest_startup_path, 1);
	}
	if (fc_rel->cheapest_total_path)
	{
		printf("\n\tcheapest total path:\n");
		fc_print_path(fc_root, fc_rel->cheapest_total_path, 1);
	}
	printf("\n");
	fflush(stdout);
}

#endif							/* OPTIMIZER_DEBUG */
